From dff491251a4ad2f10c2d21db3ec85b6e99580841 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Fri, 31 May 2024 13:38:21 -0400 Subject: [PATCH 01/13] feat: adds new side navigation component for unified nav design feat: adds status pill to nav component --- .../src/labs/NavAccordion.tsx | 155 ++++++++ .../odyssey-react-mui/src/labs/SideNav.tsx | 349 ++++++++++++++++++ packages/odyssey-react-mui/src/labs/index.ts | 3 + .../odyssey-labs/SideNav/SideNav.stories.tsx | 246 ++++++++++++ 4 files changed, 753 insertions(+) create mode 100644 packages/odyssey-react-mui/src/labs/NavAccordion.tsx create mode 100644 packages/odyssey-react-mui/src/labs/SideNav.tsx create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx diff --git a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx new file mode 100644 index 0000000000..a57308237d --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx @@ -0,0 +1,155 @@ +/*! + * Copyright (c) 2023-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 { ReactNode, memo } from "react"; +import type { HtmlProps } from "../HtmlProps"; +import { + Box, + Accordion as MuiAccordion, + AccordionDetails as MuiAccordionDetails, + AccordionSummary as MuiAccordionSummary, + AccordionProps as MuiAccordionProps, +} from "@mui/material"; +import { ChevronRightIcon } from "../icons.generated"; +import { Support } from "../Typography"; +import { useUniqueId } from "../useUniqueId"; +import { useOdysseyDesignTokens } from "../OdysseyDesignTokensContext"; + +export type NavAccordionProps = { + /** + * The content of the Accordion itself + */ + children: ReactNode; + /** + * Defines IDs for the header and the content of the Accordion + */ + id?: string; + /** + * The label text for the AccordionSummary + */ + label: string; + /** + * Whether the item is expanded by default + */ + isDefaultExpanded?: boolean; + /** + * Whether the item is disabled + */ + isDisabled?: boolean; + /** + * Whether the item is expanded + */ + isExpanded?: boolean; + /** + * Event fired when the expansion state of the accordion is changed + */ + onChange?: MuiAccordionProps["onChange"]; + /** + * The icon element to display at the start of the Nav Item + */ + startIcon?: ReactNode; +} & ( + | { + isExpanded: boolean; + isDefaultExpanded?: never; + } + | { + isDefaultExpanded?: boolean; + isExpanded?: never; + } +) & + Pick; + +const NavAccordion = ({ + children, + label, + id: idOverride, + isDefaultExpanded, + isDisabled, + isExpanded, + translate, + startIcon, +}: NavAccordionProps) => { + const id = useUniqueId(idOverride); + const headerId = `${id}-header`; + const contentId = `${id}-content`; + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + + } + id={headerId} + > + + + + {startIcon && startIcon} + + + {label} + + + + + + {children} + + + ); +}; + +const MemoizedNavAccordion = memo(NavAccordion); +MemoizedNavAccordion.displayName = "NavAccordion"; + +export { MemoizedNavAccordion as NavAccordion }; diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx new file mode 100644 index 0000000000..c1b7d2e279 --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -0,0 +1,349 @@ +/*! + * 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 { memo, MouseEvent, ReactElement, ReactNode } from "react"; + +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; + +import { NavAccordion } from "./NavAccordion"; + +import { Status, statusSeverityValues } from "../Status"; + +import { Box } from "../Box"; +import type { HtmlProps } from "../HtmlProps"; +import styled from "@emotion/styled"; +import { Heading6 } from "../Typography"; +import { CollapseLeftIcon } from "../icons.generated"; +import { Link } from "../Link"; + +export type SideNavItem = { + id: string; + label: string; + href: string; + target?: string; + /** + * Determines if the nav item is a section header + */ + isSectionHeader?: boolean; + /** + * The icon element to display at the start of the Nav Item + */ + startIcon?: ReactElement; + /** + * The status element to display after the label + */ + severity?: (typeof statusSeverityValues)[number]; + /** + * The label to display inside the status + */ + statusLabel?: string; + /** + * The icon element to display at the end of the Nav Item + */ + endIcon?: ReactElement; + /** + * Whether the item is expanded by default + */ + isDefaultExpanded?: boolean; + /** + * Whether the item is expanded + */ + isExpanded?: boolean; + /** + * Whether the item is active/selected + */ + selected?: boolean; + /** + * Event fired when the nav item is clicked + */ + onClick?(event: MouseEvent): void; + /** + * Lis navItems can be an array of side nav items or a link element + */ + children?: SideNavItem[]; +}; + +const SideNavContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + marginBlockEnd: odysseyDesignTokens.Spacing2, + backgroundColor: `${odysseyDesignTokens.HueNeutralWhite}`, +})); + +const SideNavHeaderContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + display: "flex", + justifyContent: "space-between", + alignItems: "center", + margin: odysseyDesignTokens.Spacing3, +})); + +const SideNavULContainer = styled.ul({ + padding: 0, + listStyle: "none", + listStyleType: "none", +}); + +const SideNavLIContainer = styled("li", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + marginLeft: `${odysseyDesignTokens.Spacing3}`, + marginRight: `${odysseyDesignTokens.Spacing3}`, + display: "flex", + alignItems: "center", + "&:hover": { + backgroundColor: `${odysseyDesignTokens.HueNeutral50}`, + cursor: "pointer", + }, + "&:last-child": { + marginBottom: `${odysseyDesignTokens.Spacing2}`, + }, +})); + +type CollapseIconProps = { + onClick?(event: MouseEvent): void; +}; + +const CollapseIcon = ({ onClick }: CollapseIconProps): ReactElement => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + return ( + + + + ); +}; + +const SideNavHeader = ({ + navHeaderText, + isCollapsible, + onClick, +}: Partial): ReactNode => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + return ( + + + {navHeaderText} + + {isCollapsible && } + + ); +}; + +const SideNavItemContent = ({ + id, + label, + href, + target, + startIcon, + severity, + statusLabel, + endIcon, + onClick, +}: SideNavItem): ReactNode => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + + + {startIcon && startIcon} + + a": { + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + "& > a:hover": { + textDecoration: "none", + }, + "& > a:visited": { + color: `${odysseyDesignTokens.TypographyColorHeading}`, + fontSize: `0.9rem`, + }, + }} + > + + {label} + + {severity && ( + + + + )} + + + {endIcon && endIcon} + + + ); +}; + +const sideNavItemsMap = (children: SideNavItem[]): ReactNode => { + return children?.map((child) => { + return SideNavItemContent(child); + }); +}; + +const SectionHeader = styled("li", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + fontSize: `0.7rem`, + fontWeight: "700", + color: `${odysseyDesignTokens.HueNeutral600}`, + paddingTop: `${odysseyDesignTokens.Spacing3}`, + paddingBottom: `${odysseyDesignTokens.Spacing3}`, + marginLeft: `${odysseyDesignTokens.Spacing3}`, + textTransform: "uppercase", +})); + +export type SideNavProps = { + /** + * Side Nav header text that is usually reserved to show the App name + */ + navHeaderText: string; + /** + * Determines whether the side nav is collapsible + */ + isCollapsible: boolean; + /** + * Determines whether the side nav is collapsible + */ + onClick?(event: MouseEvent): void; + /** + * Nav items in the side nav + */ + sideNavItems: SideNavItem[]; +} & Pick; + +const SideNav = ({ + navHeaderText, + isCollapsible, + onClick, + sideNavItems, +}: SideNavProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + return ( + + + + + {sideNavItems?.map((item) => { + const { + id, + label, + isSectionHeader, + startIcon, + children, + isDefaultExpanded, + } = item; + if (isSectionHeader) { + return ( + + {label} + + ); + } else if (children) { + return ( + + + + {sideNavItemsMap(children)} + + + + ); + } else { + return SideNavItemContent(item); + } + })} + + + + ); +}; + +const MemoizedSideNav = memo(SideNav); +MemoizedSideNav.displayName = "SideNav"; + +export { MemoizedSideNav as SideNav }; diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts index 0e10af7f0f..c6b2208783 100644 --- a/packages/odyssey-react-mui/src/labs/index.ts +++ b/packages/odyssey-react-mui/src/labs/index.ts @@ -32,3 +32,6 @@ export * from "./PaginatedTable"; export * from "./GroupPicker"; export * from "./Switch"; + +export * from "./NavAccordion"; +export * from "./SideNav"; 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 new file mode 100644 index 0000000000..852e51da85 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/SideNav/SideNav.stories.tsx @@ -0,0 +1,246 @@ +/*! + * Copyright (c) 2021-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 { SideNav, SideNavProps } from "@okta/odyssey-react-mui/labs"; +import { Meta, StoryObj } from "@storybook/react"; +import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { + AppsIcon, + ClockIcon, + LockIcon, + SettingsIcon, + CopyIcon, + BugIcon, + HomeIcon, + CloseIcon, + CalendarIcon, + CallIcon, + CheckIcon, + AddCircleIcon, + DownloadIcon, + ChatIcon, + RefreshIcon, + UserIcon, + DirectoryIcon, + GlobeIcon, + IdpIcon, + InformationCircleIcon, + InformationCircleFilledIcon, + ServerIcon, +} from "@okta/odyssey-react-mui/icons"; + +const storybookMeta: Meta = { + title: "Labs Components/SideNav", + component: SideNav, + argTypes: { + navHeaderText: { + control: "text", + description: "The label text for the Nav link", + table: { + type: { + summary: "string", + }, + }, + }, + isCollapsible: { + control: "boolean", + description: "Controls whether the side nav is collapsible", + table: { + type: { + summary: "boolean", + }, + }, + }, + }, + args: { + navHeaderText: "Admin", + isCollapsible: false, + onClick: () => { + console.log("collapse clicked!"); + }, + sideNavItems: [ + { + id: "AddNewFolder", + href: "/?path=/story/labs-components-switch--default", + label: "Add new folder", + endIcon: , + }, + { + id: "item0-0", + label: "Admin", + href: "", + isSectionHeader: true, + }, + { + id: "item1", + href: "/", + label: "Dashboard", + startIcon: , + children: [ + { + id: "item1-1", + href: "/", + label: "Dashboard", + startIcon: , + }, + { + id: "item1-2", + href: "/", + label: "Start", + startIcon: , + endIcon: , + }, + { + id: "item1-3", + href: "/", + label: "Onboarding", + startIcon: , + }, + { + id: "item1-4", + href: "/", + label: "Tasks", + startIcon: , + }, + { + id: "item1-5", + href: "/", + label: "Getting Started", + startIcon: , + endIcon: , + }, + ], + }, + { + id: "item0-1", + href: "/", + label: "Users", + startIcon: , + }, + { + id: "item0-2", + href: "/", + label: "Profiles", + startIcon: , + }, + { + id: "item0-3", + label: "Resource Management", + href: "", + isSectionHeader: true, + }, + { + id: "item0-1-2", + href: "/", + label: "Directory", + startIcon: , + }, + { + id: "item2", + href: "/", + label: "Applications", + startIcon: , + }, + { + id: "item3-2-1", + href: "/", + label: "Kubernetes", + startIcon: , + severity: "success", + statusLabel: "success", + }, + { + id: "item5", + href: "/", + label: "Reports", + startIcon: , + endIcon: , + }, + { + id: "item3-1-2", + href: "/", + label: "Workflows", + target: "_blank", + startIcon: , + }, + { + id: "item3-0", + label: "Security Administration", + href: "", + isSectionHeader: true, + }, + { + id: "item3", + href: "/", + label: "Security", + startIcon: , + }, + { + id: "item4", + href: "/", + label: "Settings", + startIcon: , + children: [ + { + id: "item4-1", + href: "/", + label: "General", + startIcon: , + endIcon: , + }, + { + id: "item4-3", + href: "/", + label: "Custom Login page", + startIcon: , + }, + { + id: "item4-2", + href: "/", + label: "Custom Domain", + startIcon: , + endIcon: , + }, + { + id: "item4-4", + href: "/", + label: "Authentication Policies Rules", + startIcon: , + }, + { + id: "item4-5", + href: "/", + label: "IDP Configuration", + startIcon: , + }, + ], + }, + ], + }, + decorators: [MuiThemeDecorator], + tags: ["autodocs"], +}; + +export default storybookMeta; + +export const Default: StoryObj = { + render: (props: SideNavProps) => { + return ( + + ); + }, +}; From 55339b73b471300c3ca73dc7581cd6d635995fb5 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Tue, 25 Jun 2024 18:14:13 -0400 Subject: [PATCH 02/13] fix: refactor, review comments, use odyssey tokens --- .../src/labs/NavAccordion.tsx | 61 ++-- .../odyssey-react-mui/src/labs/SideNav.tsx | 277 ++++++++++++------ .../odyssey-labs/SideNav/SideNav.stories.tsx | 41 ++- 3 files changed, 262 insertions(+), 117 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx index a57308237d..02e5348e44 100644 --- a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx @@ -22,7 +22,11 @@ import { import { ChevronRightIcon } from "../icons.generated"; import { Support } from "../Typography"; import { useUniqueId } from "../useUniqueId"; -import { useOdysseyDesignTokens } from "../OdysseyDesignTokensContext"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; +import styled from "@emotion/styled"; export type NavAccordionProps = { /** @@ -69,6 +73,21 @@ export type NavAccordionProps = { ) & Pick; +const AccordionLabelContainer = styled("span", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isIconVisible", +})<{ + odysseyDesignTokens: DesignTokens; + isIconVisible: boolean; +}>(({ odysseyDesignTokens, isIconVisible }) => ({ + width: "100%", + marginLeft: `${isIconVisible ? odysseyDesignTokens.Spacing3 : 0}`, + fontSize: `${odysseyDesignTokens.TypographyScale0}`, + fontWeight: `${odysseyDesignTokens.TypographyWeightHeading}`, + color: `${odysseyDesignTokens.TypographyColorHeading}`, + alignSelf: "center", +})); + const NavAccordion = ({ children, label, @@ -97,7 +116,10 @@ const NavAccordion = ({ > } @@ -110,28 +132,23 @@ const NavAccordion = ({ alignItems: "center", }} > - - {startIcon && startIcon} - - + {startIcon} + + )} + {label} - + diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx index c1b7d2e279..7cbca096e4 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { memo, MouseEvent, ReactElement, ReactNode } from "react"; +import { memo, useMemo, MouseEvent, ReactElement, ReactNode } from "react"; import { DesignTokens, @@ -33,10 +33,6 @@ export type SideNavItem = { label: string; href: string; target?: string; - /** - * Determines if the nav item is a section header - */ - isSectionHeader?: boolean; /** * The icon element to display at the start of the Nav Item */ @@ -69,17 +65,31 @@ export type SideNavItem = { * Event fired when the nav item is clicked */ onClick?(event: MouseEvent): void; - /** - * Lis navItems can be an array of side nav items or a link element - */ - children?: SideNavItem[]; -}; +} & ( + | { + /** + * Determines if the side nav item is a section header + */ + isSectionHeader: true; + children?: never; + } + | { + isSectionHeader?: false; + /** + * An array of side nav items + */ + children?: SideNavItem[]; + } +); const SideNavContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - marginBlockEnd: odysseyDesignTokens.Spacing2, backgroundColor: `${odysseyDesignTokens.HueNeutralWhite}`, + minWidth: "12rem", + display: "flex", + flexDirection: "column", + height: "95vh", })); const SideNavHeaderContainer = styled("div", { @@ -88,22 +98,22 @@ const SideNavHeaderContainer = styled("div", { display: "flex", justifyContent: "space-between", alignItems: "center", - margin: odysseyDesignTokens.Spacing3, + padding: `${odysseyDesignTokens.Spacing4}`, + height: "8vh", })); -const SideNavULContainer = styled.ul({ +const SideNavListContainer = styled.ul({ padding: 0, listStyle: "none", listStyleType: "none", }); -const SideNavLIContainer = styled("li", { +const SideNavListItemContainer = styled("li", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - marginLeft: `${odysseyDesignTokens.Spacing3}`, - marginRight: `${odysseyDesignTokens.Spacing3}`, display: "flex", alignItems: "center", + minHeight: "48px", "&:hover": { backgroundColor: `${odysseyDesignTokens.HueNeutral50}`, cursor: "pointer", @@ -125,8 +135,8 @@ const CollapseIcon = ({ onClick }: CollapseIconProps): ReactElement => { sx={{ width: "24px", height: "24px", - border: `1px solid ${odysseyDesignTokens.HueNeutral300}`, - borderRadius: "4px", + border: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral300}`, + borderRadius: `${odysseyDesignTokens.BorderRadiusTight}`, cursor: "pointer", padding: `${odysseyDesignTokens.Spacing1}`, }} @@ -148,8 +158,11 @@ const CollapseIcon = ({ onClick }: CollapseIconProps): ReactElement => { const SideNavHeader = ({ navHeaderText, isCollapsible, - onClick, -}: Partial): ReactNode => { + onCollapse, +}: Pick< + SideNavProps, + "navHeaderText" | "isCollapsible" | "onCollapse" +>): ReactNode => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( @@ -160,11 +173,53 @@ const SideNavHeader = ({ > {navHeaderText} - {isCollapsible && } + {isCollapsible && } ); }; +const SideNavFooterContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + paddingTop: `${odysseyDesignTokens.Spacing2}`, + paddingBottom: `${odysseyDesignTokens.Spacing2}`, + height: "4vh", + display: "flex", + justifyContent: "center", + flexWrap: "wrap", + alignItems: "center", + fontSize: `${odysseyDesignTokens.TypographySizeOverline}`, + "& > a": { + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + "& > a:hover": { + textDecoration: "none", + }, + "& > a:visited": { + color: `${odysseyDesignTokens.TypographyColorHeading}`, + }, + "& > a:not(:last-child)::after": { + marginLeft: odysseyDesignTokens.Spacing4, + marginRight: odysseyDesignTokens.Spacing4, + color: `${odysseyDesignTokens.HueNeutral300}`, + content: '" | "', + }, +})); + +export type SideNavFooterItem = { + id: string; + label: string; + href: string; +}; + +const SideNavFooter = ({ id, label, href }: SideNavFooterItem) => { + return ( + + {label} + + ); +}; + const SideNavItemContent = ({ id, label, @@ -175,75 +230,92 @@ const SideNavItemContent = ({ statusLabel, endIcon, onClick, -}: SideNavItem): ReactNode => { +}: Pick< + SideNavItem, + | "id" + | "label" + | "href" + | "target" + | "startIcon" + | "severity" + | "statusLabel" + | "endIcon" + | "onClick" +>) => { const odysseyDesignTokens = useOdysseyDesignTokens(); - return ( - - {startIcon && startIcon} - - a": { - color: `${odysseyDesignTokens.TypographyColorHeading} !important`, - }, - "& > a:hover": { - textDecoration: "none", - }, - "& > a:visited": { - color: `${odysseyDesignTokens.TypographyColorHeading}`, - fontSize: `0.9rem`, - }, }} > - - {label} - - {severity && ( + {startIcon && ( - + {startIcon} )} + a": { + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + "& > a:hover": { + textDecoration: "none", + }, + "& > a:visited": { + color: `${odysseyDesignTokens.TypographyColorHeading}`, + fontSize: `${odysseyDesignTokens.TypographyScale0}`, + }, + }} + > + + {label} + + {severity && ( + + + + )} + + + {endIcon && endIcon} + - - {endIcon && endIcon} - - + ); }; -const sideNavItemsMap = (children: SideNavItem[]): ReactNode => { - return children?.map((child) => { - return SideNavItemContent(child); - }); -}; - const SectionHeader = styled("li", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ @@ -252,7 +324,7 @@ const SectionHeader = styled("li", { color: `${odysseyDesignTokens.HueNeutral600}`, paddingTop: `${odysseyDesignTokens.Spacing3}`, paddingBottom: `${odysseyDesignTokens.Spacing3}`, - marginLeft: `${odysseyDesignTokens.Spacing3}`, + paddingLeft: `${odysseyDesignTokens.Spacing4}`, textTransform: "uppercase", })); @@ -268,35 +340,51 @@ export type SideNavProps = { /** * Determines whether the side nav is collapsible */ - onClick?(event: MouseEvent): void; + onCollapse?(event: MouseEvent): void; /** * Nav items in the side nav */ sideNavItems: SideNavItem[]; + /** + * Footer items in the side nav + */ + footerItems?: SideNavFooterItem[]; } & Pick; const SideNav = ({ navHeaderText, isCollapsible, - onClick, + onCollapse, sideNavItems, + footerItems, }: SideNavProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const processedSideNavItems = useMemo( + () => + sideNavItems.map((item) => ({ + ...item, + children: item.children?.map((childProps) => ( + + )), + })), + [sideNavItems], + ); return ( - - {sideNavItems?.map((item) => { + + {processedSideNavItems?.map((item) => { const { id, label, @@ -317,7 +405,7 @@ const SideNav = ({ ); } else if (children) { return ( - - - {sideNavItemsMap(children)} - + + {children} + - + ); } else { - return SideNavItemContent(item); + return ( + + ); } })} - + + + {footerItems?.map((item) => ( + + ))} + ); }; 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 852e51da85..b093f89d1b 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 @@ -16,11 +16,11 @@ import { MuiThemeDecorator } from "../../../../.storybook/components"; import { AppsIcon, ClockIcon, - LockIcon, SettingsIcon, CopyIcon, - BugIcon, HomeIcon, + LockIcon, + BugIcon, CloseIcon, CalendarIcon, CallIcon, @@ -36,6 +36,7 @@ import { InformationCircleIcon, InformationCircleFilledIcon, ServerIcon, + ExpandLeftIcon, } from "@okta/odyssey-react-mui/icons"; const storybookMeta: Meta = { @@ -63,10 +64,8 @@ const storybookMeta: Meta = { }, args: { navHeaderText: "Admin", - isCollapsible: false, - onClick: () => { - console.log("collapse clicked!"); - }, + isCollapsible: true, + onCollapse: () => {}, sideNavItems: [ { id: "AddNewFolder", @@ -84,13 +83,12 @@ const storybookMeta: Meta = { id: "item1", href: "/", label: "Dashboard", - startIcon: , children: [ { id: "item1-1", href: "/", - label: "Dashboard", - startIcon: , + label: "Home", + startIcon: , }, { id: "item1-2", @@ -110,6 +108,7 @@ const storybookMeta: Meta = { href: "/", label: "Tasks", startIcon: , + endIcon: , }, { id: "item1-5", @@ -182,7 +181,7 @@ const storybookMeta: Meta = { id: "item3", href: "/", label: "Security", - startIcon: , + endIcon: , }, { id: "item4", @@ -225,6 +224,23 @@ const storybookMeta: Meta = { ], }, ], + footerItems: [ + { + id: "footer-item-1", + label: "Docs", + href: "/", + }, + { + id: "footer-item-2", + label: "Privacy", + href: "/", + }, + { + id: "footer-item-3", + label: "Security", + href: "/", + }, + ], }, decorators: [MuiThemeDecorator], tags: ["autodocs"], @@ -232,14 +248,15 @@ const storybookMeta: Meta = { export default storybookMeta; -export const Default: StoryObj = { +export const Default: StoryObj = { render: (props: SideNavProps) => { return ( ); }, From 284260a0ab3ea4dc8a3f922d3068a6e0218127d2 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Wed, 26 Jun 2024 17:42:53 -0400 Subject: [PATCH 03/13] fix: refactor, use odyssey tokens --- .../src/labs/NavAccordion.tsx | 11 +- .../odyssey-react-mui/src/labs/SideNav.tsx | 181 ++++++++++-------- .../odyssey-labs/SideNav/SideNav.stories.tsx | 17 +- 3 files changed, 110 insertions(+), 99 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx index 02e5348e44..c576ee1e36 100644 --- a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx @@ -82,9 +82,9 @@ const AccordionLabelContainer = styled("span", { }>(({ odysseyDesignTokens, isIconVisible }) => ({ width: "100%", marginLeft: `${isIconVisible ? odysseyDesignTokens.Spacing3 : 0}`, - fontSize: `${odysseyDesignTokens.TypographyScale0}`, - fontWeight: `${odysseyDesignTokens.TypographyWeightHeading}`, - color: `${odysseyDesignTokens.TypographyColorHeading}`, + fontSize: odysseyDesignTokens.TypographyScale0, + fontWeight: odysseyDesignTokens.TypographyWeightHeading, + color: odysseyDesignTokens.TypographyColorHeading, alignSelf: "center", })); @@ -136,8 +136,7 @@ const NavAccordion = ({ {startIcon} @@ -157,7 +156,7 @@ const NavAccordion = ({ sx={{ paddingTop: "0", paddingBottom: "0", - paddingLeft: `${odysseyDesignTokens.Spacing2}`, + paddingLeft: odysseyDesignTokens.Spacing2, }} > {children} diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx index 7cbca096e4..852ad01d88 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -85,7 +85,7 @@ export type SideNavItem = { const SideNavContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - backgroundColor: `${odysseyDesignTokens.HueNeutralWhite}`, + backgroundColor: odysseyDesignTokens.HueNeutralWhite, minWidth: "12rem", display: "flex", flexDirection: "column", @@ -98,57 +98,37 @@ const SideNavHeaderContainer = styled("div", { display: "flex", justifyContent: "space-between", alignItems: "center", - padding: `${odysseyDesignTokens.Spacing4}`, - height: "8vh", + paddingLeft: odysseyDesignTokens.Spacing4, + paddingRight: odysseyDesignTokens.Spacing4, + paddingTop: odysseyDesignTokens.Spacing3, })); -const SideNavListContainer = styled.ul({ - padding: 0, - listStyle: "none", - listStyleType: "none", -}); - -const SideNavListItemContainer = styled("li", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - display: "flex", - alignItems: "center", - minHeight: "48px", - "&:hover": { - backgroundColor: `${odysseyDesignTokens.HueNeutral50}`, - cursor: "pointer", - }, - "&:last-child": { - marginBottom: `${odysseyDesignTokens.Spacing2}`, - }, -})); - -type CollapseIconProps = { - onClick?(event: MouseEvent): void; -}; - -const CollapseIcon = ({ onClick }: CollapseIconProps): ReactElement => { +const CollapseIcon = ({ onClick }: { onClick?(): void }): ReactElement => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( @@ -168,7 +148,7 @@ const SideNavHeader = ({ {navHeaderText} @@ -178,17 +158,78 @@ const SideNavHeader = ({ ); }; +const SectionHeader = styled("li", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + fontSize: odysseyDesignTokens.TypographySizeOverline, + fontWeight: odysseyDesignTokens.TypographyWeightHeadingBold, + color: odysseyDesignTokens.HueNeutral600, + paddingTop: odysseyDesignTokens.Spacing3, + paddingBottom: odysseyDesignTokens.Spacing3, + paddingLeft: odysseyDesignTokens.Spacing4, + textTransform: "uppercase", +})); + +const SideNavItemLabelContainer = styled("div", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isIconVisible", +})<{ + odysseyDesignTokens: DesignTokens; + isIconVisible: boolean; +}>(({ odysseyDesignTokens, isIconVisible }) => ({ + width: "100%", + display: "inline-flex", + flexWrap: "wrap", + alignItems: "center", + fontSize: odysseyDesignTokens.TypographyScale0, + fontWeight: odysseyDesignTokens.TypographyWeightHeading, + paddingTop: odysseyDesignTokens.Spacing3, + paddingBottom: odysseyDesignTokens.Spacing3, + marginLeft: `${isIconVisible ? odysseyDesignTokens.Spacing3 : 0}`, + "& > a": { + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + "& > a:hover": { + textDecoration: "none", + }, + "& > a:visited": { + color: odysseyDesignTokens.TypographyColorHeading, + fontSize: odysseyDesignTokens.TypographyScale0, + }, +})); + +const SideNavListContainer = styled.ul({ + padding: 0, + listStyle: "none", + listStyleType: "none", +}); + +const SideNavListItemContainer = styled("li", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + display: "flex", + alignItems: "center", + minHeight: "48px", + "&:hover": { + backgroundColor: odysseyDesignTokens.HueNeutral50, + cursor: "pointer", + }, + "&:last-child": { + marginBottom: odysseyDesignTokens.Spacing2, + }, +})); + const SideNavFooterContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - paddingTop: `${odysseyDesignTokens.Spacing2}`, - paddingBottom: `${odysseyDesignTokens.Spacing2}`, - height: "4vh", + paddingTop: odysseyDesignTokens.Spacing2, + paddingBottom: odysseyDesignTokens.Spacing2, + height: "auto", display: "flex", justifyContent: "center", flexWrap: "wrap", alignItems: "center", - fontSize: `${odysseyDesignTokens.TypographySizeOverline}`, + fontSize: odysseyDesignTokens.TypographySizeOverline, "& > a": { color: `${odysseyDesignTokens.TypographyColorHeading} !important`, }, @@ -196,12 +237,12 @@ const SideNavFooterContainer = styled("div", { textDecoration: "none", }, "& > a:visited": { - color: `${odysseyDesignTokens.TypographyColorHeading}`, + color: odysseyDesignTokens.TypographyColorHeading, }, "& > a:not(:last-child)::after": { marginLeft: odysseyDesignTokens.Spacing4, marginRight: odysseyDesignTokens.Spacing4, - color: `${odysseyDesignTokens.HueNeutral300}`, + color: odysseyDesignTokens.HueNeutral300, content: '" | "', }, })); @@ -251,8 +292,8 @@ const SideNavItemContent = ({ > )} - a": { - color: `${odysseyDesignTokens.TypographyColorHeading} !important`, - }, - "& > a:hover": { - textDecoration: "none", - }, - "& > a:visited": { - color: `${odysseyDesignTokens.TypographyColorHeading}`, - fontSize: `${odysseyDesignTokens.TypographyScale0}`, - }, - }} + {label} @@ -295,16 +318,15 @@ const SideNavItemContent = ({ {severity && ( )} - + prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - fontSize: `0.7rem`, - fontWeight: "700", - color: `${odysseyDesignTokens.HueNeutral600}`, - paddingTop: `${odysseyDesignTokens.Spacing3}`, - paddingBottom: `${odysseyDesignTokens.Spacing3}`, - paddingLeft: `${odysseyDesignTokens.Spacing4}`, - textTransform: "uppercase", -})); - export type SideNavProps = { /** * Side Nav header text that is usually reserved to show the App name @@ -338,9 +348,9 @@ export type SideNavProps = { */ isCollapsible: boolean; /** - * Determines whether the side nav is collapsible + * Triggers whether the side nav is collapsed */ - onCollapse?(event: MouseEvent): void; + onCollapse?(): void; /** * Nav items in the side nav */ @@ -393,6 +403,7 @@ const SideNav = ({ children, isDefaultExpanded, } = item; + if (isSectionHeader) { return ( - + {children} 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 b093f89d1b..6debf6bec8 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 @@ -79,16 +79,23 @@ const storybookMeta: Meta = { href: "", isSectionHeader: true, }, + { + id: "item0-1", + href: "/", + label: "Users", + startIcon: , + }, { id: "item1", href: "/", label: "Dashboard", + startIcon: , children: [ { id: "item1-1", href: "/", label: "Home", - startIcon: , + startIcon: , }, { id: "item1-2", @@ -101,7 +108,7 @@ const storybookMeta: Meta = { id: "item1-3", href: "/", label: "Onboarding", - startIcon: , + startIcon: , }, { id: "item1-4", @@ -119,12 +126,6 @@ const storybookMeta: Meta = { }, ], }, - { - id: "item0-1", - href: "/", - label: "Users", - startIcon: , - }, { id: "item0-2", href: "/", From 32d0c85bb2c7d00525f50b0cb0dbe5d6850e6a5f Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Thu, 27 Jun 2024 17:14:20 -0400 Subject: [PATCH 04/13] fix: adds selected feature --- .../src/labs/NavAccordion.tsx | 17 +-- .../odyssey-react-mui/src/labs/SideNav.tsx | 134 ++++++++++-------- .../src/theme/components.tsx | 8 ++ .../odyssey-labs/SideNav/SideNav.stories.tsx | 46 +++++- 4 files changed, 124 insertions(+), 81 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx index c576ee1e36..fe34b7e045 100644 --- a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx @@ -81,7 +81,7 @@ const AccordionLabelContainer = styled("span", { isIconVisible: boolean; }>(({ odysseyDesignTokens, isIconVisible }) => ({ width: "100%", - marginLeft: `${isIconVisible ? odysseyDesignTokens.Spacing3 : 0}`, + marginLeft: isIconVisible ? odysseyDesignTokens.Spacing3 : 0, fontSize: odysseyDesignTokens.TypographyScale0, fontWeight: odysseyDesignTokens.TypographyWeightHeading, color: odysseyDesignTokens.TypographyColorHeading, @@ -132,16 +132,7 @@ const NavAccordion = ({ alignItems: "center", }} > - {startIcon && ( - - {startIcon} - - )} + {startIcon && startIcon} diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx index 852ad01d88..5be5579395 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -31,7 +31,6 @@ import { Link } from "../Link"; export type SideNavItem = { id: string; label: string; - href: string; target?: string; /** * The icon element to display at the start of the Nav Item @@ -57,10 +56,14 @@ export type SideNavItem = { * Whether the item is expanded */ isExpanded?: boolean; + /** + * Whether the item is disabled + */ + isDisabled?: boolean; /** * Whether the item is active/selected */ - selected?: boolean; + isSelected?: boolean; /** * Event fired when the nav item is clicked */ @@ -71,10 +74,12 @@ export type SideNavItem = { * Determines if the side nav item is a section header */ isSectionHeader: true; + href?: never; children?: never; } | { isSectionHeader?: false; + href: string; /** * An array of side nav items */ @@ -158,6 +163,12 @@ const SideNavHeader = ({ ); }; +const SideNavListContainer = styled.ul({ + padding: 0, + listStyle: "none", + listStyleType: "none", +}); + const SectionHeader = styled("li", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ @@ -172,25 +183,28 @@ const SectionHeader = styled("li", { const SideNavItemLabelContainer = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isIconVisible", + prop !== "odysseyDesignTokens" && + prop !== "isIconVisible" && + prop !== "isDisabled", })<{ odysseyDesignTokens: DesignTokens; isIconVisible: boolean; -}>(({ odysseyDesignTokens, isIconVisible }) => ({ + isDisabled?: boolean; +}>(({ odysseyDesignTokens, isIconVisible, isDisabled }) => ({ width: "100%", - display: "inline-flex", + display: "flex", flexWrap: "wrap", alignItems: "center", fontSize: odysseyDesignTokens.TypographyScale0, fontWeight: odysseyDesignTokens.TypographyWeightHeading, - paddingTop: odysseyDesignTokens.Spacing3, - paddingBottom: odysseyDesignTokens.Spacing3, - marginLeft: `${isIconVisible ? odysseyDesignTokens.Spacing3 : 0}`, + marginLeft: isIconVisible ? odysseyDesignTokens.Spacing3 : 0, "& > a": { color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + pointerEvents: isDisabled ? "none" : "auto", }, "& > a:hover": { textDecoration: "none", + cursor: isDisabled ? "default" : "pointer", }, "& > a:visited": { color: odysseyDesignTokens.TypographyColorHeading, @@ -198,25 +212,47 @@ const SideNavItemLabelContainer = styled("div", { }, })); -const SideNavListContainer = styled.ul({ - padding: 0, - listStyle: "none", - listStyleType: "none", -}); - const SideNavListItemContainer = styled("li", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && + prop !== "isSelected" && + prop !== "isDisabled", +})<{ + odysseyDesignTokens: DesignTokens; + isSelected?: boolean; + disabled?: boolean; + isDisabled?: boolean; +}>(({ 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: odysseyDesignTokens.HueNeutral50, - cursor: "pointer", + backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "auto", }, "&:last-child": { marginBottom: odysseyDesignTokens.Spacing2, }, + "& > a": { + display: "flex", + alignItems: "center", + width: "100%", + paddingRight: odysseyDesignTokens.Spacing4, + paddingLeft: odysseyDesignTokens.Spacing4, + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + pointerEvents: isDisabled ? "none" : "auto", + }, + "& > a:hover": { + textDecoration: "none", + cursor: isDisabled ? "default" : "pointer", + }, + "& > a:visited": { + color: odysseyDesignTokens.TypographyColorHeading, + fontSize: odysseyDesignTokens.TypographyScale0, + }, })); const SideNavFooterContainer = styled("div", { @@ -271,6 +307,8 @@ const SideNavItemContent = ({ statusLabel, endIcon, onClick, + isSelected, + isDisabled, }: Pick< SideNavItem, | "id" @@ -282,39 +320,27 @@ const SideNavItemContent = ({ | "statusLabel" | "endIcon" | "onClick" + | "isSelected" + | "isDisabled" >) => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( - - {startIcon && ( - - {startIcon} - - )} + + {startIcon && startIcon} - - {label} - + {label} {severity && ( )} - - {endIcon && endIcon} - - + {endIcon && endIcon} + ); }; @@ -375,7 +394,7 @@ const SideNav = ({ sideNavItems.map((item) => ({ ...item, children: item.children?.map((childProps) => ( - + )), })), [sideNavItems], @@ -402,6 +421,7 @@ const SideNav = ({ startIcon, children, isDefaultExpanded, + isDisabled, } = item; if (isSectionHeader) { @@ -425,6 +445,7 @@ const SideNav = ({ label={label} isDefaultExpanded={isDefaultExpanded} startIcon={startIcon} + isDisabled={isDisabled} > {children} @@ -433,20 +454,7 @@ const SideNav = ({ ); } else { - return ( - - ); + return ; } })} diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index c5c61f76ac..2ab8d158ca 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -121,6 +121,14 @@ export const components = ({ outlineStyle: "solid", zIndex: 1, }, + svg: { + fontSize: "1em", + height: "1em", + position: "relative", + insetBlockStart: "-0.0625em", + verticalAlign: "middle", + width: "1em", + }, }), content: () => ({ marginBlock: 0, 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 6debf6bec8..c6a0ba6524 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 @@ -19,6 +19,7 @@ import { SettingsIcon, CopyIcon, HomeIcon, + Fido2Icon, LockIcon, BugIcon, CloseIcon, @@ -45,7 +46,7 @@ const storybookMeta: Meta = { argTypes: { navHeaderText: { control: "text", - description: "The label text for the Nav link", + description: "Header text for the side nav", table: { type: { summary: "string", @@ -61,6 +62,25 @@ const storybookMeta: Meta = { }, }, }, + onCollapse: { + description: "Callback to be triggered when the side nav is collapsed", + }, + sideNavItems: { + description: "", + table: { + type: { + summary: "Array", + }, + }, + }, + footerItems: { + description: "", + table: { + type: { + summary: "Array", + }, + }, + }, }, args: { navHeaderText: "Admin", @@ -76,7 +96,6 @@ const storybookMeta: Meta = { { id: "item0-0", label: "Admin", - href: "", isSectionHeader: true, }, { @@ -90,6 +109,7 @@ const storybookMeta: Meta = { href: "/", label: "Dashboard", startIcon: , + isDisabled: true, children: [ { id: "item1-1", @@ -131,11 +151,12 @@ const storybookMeta: Meta = { href: "/", label: "Profiles", startIcon: , + endIcon: , + isDisabled: true, }, { id: "item0-3", label: "Resource Management", - href: "", isSectionHeader: true, }, { @@ -149,6 +170,7 @@ const storybookMeta: Meta = { href: "/", label: "Applications", startIcon: , + isSelected: true, }, { id: "item3-2-1", @@ -157,17 +179,31 @@ const storybookMeta: Meta = { startIcon: , severity: "success", statusLabel: "success", + endIcon: , }, { id: "item5", href: "/", label: "Reports", - startIcon: , endIcon: , }, + { + id: "item3-1-0", + href: "/", + label: "Identify Governance", + target: "_blank", + isDisabled: true, + startIcon: , + }, { id: "item3-1-2", href: "/", + label: "Gateways", + startIcon: , + }, + { + id: "item3-1-3", + href: "/", label: "Workflows", target: "_blank", startIcon: , @@ -175,13 +211,13 @@ const storybookMeta: Meta = { { id: "item3-0", label: "Security Administration", - href: "", isSectionHeader: true, }, { id: "item3", href: "/", label: "Security", + startIcon: , endIcon: , }, { From 7f7c59df4eaa3043e1e374e10c03287565510e59 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Fri, 28 Jun 2024 17:05:55 -0400 Subject: [PATCH 05/13] feat: adds side nav collapse option --- .../odyssey-react-mui/src/labs/SideNav.tsx | 307 ++++++++++++------ .../odyssey-labs/SideNav/SideNav.stories.tsx | 13 +- 2 files changed, 207 insertions(+), 113 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx index 5be5579395..93abba868b 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -10,7 +10,14 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { memo, useMemo, MouseEvent, ReactElement, ReactNode } from "react"; +import { + memo, + useMemo, + useState, + MouseEvent, + ReactElement, + ReactNode, +} from "react"; import { DesignTokens, @@ -25,7 +32,7 @@ import { Box } from "../Box"; import type { HtmlProps } from "../HtmlProps"; import styled from "@emotion/styled"; import { Heading6 } from "../Typography"; -import { CollapseLeftIcon } from "../icons.generated"; +import { CollapseLeftIcon, ExpandLeftIcon } from "../icons.generated"; import { Link } from "../Link"; export type SideNavItem = { @@ -87,15 +94,52 @@ export type SideNavItem = { } ); +export type SideNavFooterItem = { + id: string; + label: string; + href: string; +}; + +const SideNavCollapsedContainer = styled("div", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "sideNavCollapsed", +})( + ({ + odysseyDesignTokens, + sideNavCollapsed, + }: { + odysseyDesignTokens: DesignTokens; + sideNavCollapsed: boolean; + }) => ({ + backgroundColor: odysseyDesignTokens.HueNeutral300, + paddingTop: odysseyDesignTokens.Spacing5, + cursor: "pointer", + width: sideNavCollapsed ? "auto" : 0, + visibility: sideNavCollapsed ? "visible" : "hidden", + }), +); + const SideNavContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", -})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - backgroundColor: odysseyDesignTokens.HueNeutralWhite, - minWidth: "12rem", - display: "flex", - flexDirection: "column", - height: "95vh", -})); + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "sideNavCollapsed", +})( + ({ + odysseyDesignTokens, + sideNavCollapsed, + }: { + odysseyDesignTokens: DesignTokens; + sideNavCollapsed: boolean; + }) => ({ + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + flexDirection: "column", + display: "flex", + visibility: sideNavCollapsed ? "hidden" : "visible", + width: sideNavCollapsed ? "0" : "100%", + transitionProperty: "width, visibility", + transitionDuration: odysseyDesignTokens.TransitionDurationMain, + transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, + }), +); const SideNavHeaderContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", @@ -106,37 +150,49 @@ const SideNavHeaderContainer = styled("div", { paddingLeft: odysseyDesignTokens.Spacing4, paddingRight: odysseyDesignTokens.Spacing4, paddingTop: odysseyDesignTokens.Spacing3, + paddingBottom: odysseyDesignTokens.Spacing3, })); const CollapseIcon = ({ onClick }: { onClick?(): void }): ReactElement => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( - { + event.key === "Enter" && onClick && onClick(); }} > - - + > + + + ); }; @@ -260,7 +316,6 @@ const SideNavFooterContainer = styled("div", { })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ paddingTop: odysseyDesignTokens.Spacing2, paddingBottom: odysseyDesignTokens.Spacing2, - height: "auto", display: "flex", justifyContent: "center", flexWrap: "wrap", @@ -283,12 +338,6 @@ const SideNavFooterContainer = styled("div", { }, })); -export type SideNavFooterItem = { - id: string; - label: string; - href: string; -}; - const SideNavFooter = ({ id, label, href }: SideNavFooterItem) => { return ( @@ -365,9 +414,9 @@ export type SideNavProps = { /** * Determines whether the side nav is collapsible */ - isCollapsible: boolean; + isCollapsible?: boolean; /** - * Triggers whether the side nav is collapsed + * Triggers when the side nav is collapsed */ onCollapse?(): void; /** @@ -387,6 +436,7 @@ const SideNav = ({ sideNavItems, footerItems, }: SideNavProps) => { + const [sideNavCollapsed, setSideNavCollapsed] = useState(false); const odysseyDesignTokens = useOdysseyDesignTokens(); const processedSideNavItems = useMemo( @@ -400,76 +450,119 @@ const SideNav = ({ [sideNavItems], ); return ( - - - + setSideNavCollapsed(!sideNavCollapsed)} + onKeyDown={(event) => { + event.key === "Enter" && setSideNavCollapsed(!sideNavCollapsed); }} > - - {processedSideNavItems?.map((item) => { - const { - id, - label, - isSectionHeader, - startIcon, - children, - isDefaultExpanded, - isDisabled, - } = item; + + + + + { + setSideNavCollapsed(!sideNavCollapsed); + onCollapse && onCollapse(); + }} + /> + + + + {processedSideNavItems?.map((item) => { + const { + id, + label, + isSectionHeader, + startIcon, + children, + isDefaultExpanded, + isDisabled, + } = item; - if (isSectionHeader) { - return ( - - {label} - - ); - } else if (children) { - return ( - - - - {children} - - - - ); - } else { - return ; - } - })} - - - - {footerItems?.map((item) => ( - - ))} - - + {label} + + ); + } else if (children) { + return ( + + + + {children} + + + + ); + } else { + return ; + } + })} + + + + + {footerItems?.map((item) => ( + + ))} + + + + ); }; 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 c6a0ba6524..2883ba180d 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 @@ -38,6 +38,7 @@ import { InformationCircleFilledIcon, ServerIcon, ExpandLeftIcon, + FolderIcon, } from "@okta/odyssey-react-mui/icons"; const storybookMeta: Meta = { @@ -195,12 +196,6 @@ const storybookMeta: Meta = { isDisabled: true, startIcon: , }, - { - id: "item3-1-2", - href: "/", - label: "Gateways", - startIcon: , - }, { id: "item3-1-3", href: "/", @@ -260,6 +255,12 @@ const storybookMeta: Meta = { }, ], }, + { + id: "item5-0", + href: "/", + label: "System Configuration", + startIcon: , + }, ], footerItems: [ { From 05be72bf0d74ca714d081a3d697fd5babb033bf2 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Mon, 1 Jul 2024 17:35:59 -0400 Subject: [PATCH 06/13] fix: review comments refactor --- .../src/labs/NavAccordion.tsx | 60 ++-- .../odyssey-react-mui/src/labs/SideNav.tsx | 289 ++++++++++-------- .../src/theme/components.tsx | 22 +- .../odyssey-labs/SideNav/SideNav.stories.tsx | 19 +- 4 files changed, 209 insertions(+), 181 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx index fe34b7e045..9f50368f8f 100644 --- a/packages/odyssey-react-mui/src/labs/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/NavAccordion.tsx @@ -10,37 +10,37 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { ReactNode, memo } from "react"; -import type { HtmlProps } from "../HtmlProps"; +import styled from "@emotion/styled"; import { - Box, Accordion as MuiAccordion, AccordionDetails as MuiAccordionDetails, AccordionSummary as MuiAccordionSummary, AccordionProps as MuiAccordionProps, } from "@mui/material"; +import { ReactNode, memo } from "react"; + +import type { HtmlProps } from "../HtmlProps"; import { ChevronRightIcon } from "../icons.generated"; -import { Support } from "../Typography"; -import { useUniqueId } from "../useUniqueId"; import { DesignTokens, useOdysseyDesignTokens, } from "../OdysseyDesignTokensContext"; -import styled from "@emotion/styled"; +import { Support } from "../Typography"; +import { useUniqueId } from "../useUniqueId"; export type NavAccordionProps = { /** * The content of the Accordion itself */ children: ReactNode; - /** - * Defines IDs for the header and the content of the Accordion - */ - id?: string; /** * The label text for the AccordionSummary */ label: string; + /** + * Defines IDs for the header and the content of the Accordion + */ + id?: string; /** * Whether the item is expanded by default */ @@ -81,11 +81,10 @@ const AccordionLabelContainer = styled("span", { isIconVisible: boolean; }>(({ odysseyDesignTokens, isIconVisible }) => ({ width: "100%", - marginLeft: isIconVisible ? odysseyDesignTokens.Spacing3 : 0, + marginLeft: isIconVisible ? odysseyDesignTokens.Spacing2 : 0, fontSize: odysseyDesignTokens.TypographyScale0, fontWeight: odysseyDesignTokens.TypographyWeightHeading, color: odysseyDesignTokens.TypographyColorHeading, - alignSelf: "center", })); const NavAccordion = ({ @@ -109,46 +108,27 @@ const NavAccordion = ({ disabled={isDisabled} disableGutters expanded={isExpanded} - sx={{ - border: "0 !important", - width: "100%", - }} + className="nav-accordion" > } id={headerId} > - - {startIcon && startIcon} - - {label} - - + {label} + {children} diff --git a/packages/odyssey-react-mui/src/labs/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav.tsx index 93abba868b..22793ae814 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav.tsx @@ -10,6 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. */ +import styled from "@emotion/styled"; import { memo, useMemo, @@ -17,40 +18,25 @@ import { MouseEvent, ReactElement, ReactNode, + useCallback, } from "react"; +import { Box } from "../Box"; +import { Button } from "../Button"; +import type { HtmlProps } from "../HtmlProps"; +import { CollapseLeftIcon, ExpandLeftIcon } from "../icons.generated"; +import { Link } from "../Link"; +import { NavAccordion } from "./NavAccordion"; import { DesignTokens, useOdysseyDesignTokens, } from "../OdysseyDesignTokensContext"; - -import { NavAccordion } from "./NavAccordion"; - import { Status, statusSeverityValues } from "../Status"; - -import { Box } from "../Box"; -import type { HtmlProps } from "../HtmlProps"; -import styled from "@emotion/styled"; import { Heading6 } from "../Typography"; -import { CollapseLeftIcon, ExpandLeftIcon } from "../icons.generated"; -import { Link } from "../Link"; export type SideNavItem = { id: string; label: string; - target?: string; - /** - * The icon element to display at the start of the Nav Item - */ - startIcon?: ReactElement; - /** - * The status element to display after the label - */ - severity?: (typeof statusSeverityValues)[number]; - /** - * The label to display inside the status - */ - statusLabel?: string; /** * The icon element to display at the end of the Nav Item */ @@ -75,6 +61,19 @@ export type SideNavItem = { * Event fired when the nav item is clicked */ onClick?(event: MouseEvent): 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,47 +94,52 @@ 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, }), @@ -156,43 +160,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, + }, }} > - - - -
+