From 4142790ab20c4f1d5903b0b78d1d8f137740e463 Mon Sep 17 00:00:00 2001 From: Ganesh Somasundaram Date: Mon, 22 Jul 2024 17:19:16 -0400 Subject: [PATCH 1/7] feat: adds a generic top nav component --- .../src/icons.generated/Aura.tsx | 45 ++ .../src/icons.generated/AuraWordmark.tsx | 46 ++ .../src/icons.generated/index.ts | 2 + .../odyssey-react-mui/src/labs/TopNav.tsx | 430 ++++++++++++++++++ packages/odyssey-react-mui/src/labs/index.ts | 1 + .../odyssey-labs/TopNav/TopNav.stories.tsx | 149 ++++++ 6 files changed, 673 insertions(+) create mode 100644 packages/odyssey-react-mui/src/icons.generated/Aura.tsx create mode 100644 packages/odyssey-react-mui/src/icons.generated/AuraWordmark.tsx create mode 100644 packages/odyssey-react-mui/src/labs/TopNav.tsx create mode 100644 packages/odyssey-storybook/src/components/odyssey-labs/TopNav/TopNav.stories.tsx diff --git a/packages/odyssey-react-mui/src/icons.generated/Aura.tsx b/packages/odyssey-react-mui/src/icons.generated/Aura.tsx new file mode 100644 index 0000000000..e086553f25 --- /dev/null +++ b/packages/odyssey-react-mui/src/icons.generated/Aura.tsx @@ -0,0 +1,45 @@ +/*! + * Copyright (c) 2024-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. + */ + +// Code automatically generated by svgr; DO NOT EDIT. + +import { forwardRef } from "react"; +import { SvgIcon, type SvgIconNoChildrenProps } from "../SvgIcon"; + +export type AuraIconProps = SvgIconNoChildrenProps; + +export const AuraIcon = forwardRef( + (props, ref) => { + return ( + + <> + + + + ); + }, +); + +AuraIcon.displayName = "AuraIcon"; diff --git a/packages/odyssey-react-mui/src/icons.generated/AuraWordmark.tsx b/packages/odyssey-react-mui/src/icons.generated/AuraWordmark.tsx new file mode 100644 index 0000000000..f45f6829e9 --- /dev/null +++ b/packages/odyssey-react-mui/src/icons.generated/AuraWordmark.tsx @@ -0,0 +1,46 @@ +/*! + * Copyright (c) 2024-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. + */ + +// Code automatically generated by svgr; DO NOT EDIT. + +import { forwardRef } from "react"; +import { SvgIcon, type SvgIconNoChildrenProps } from "../SvgIcon"; + +export type AuraWordmarkIconProps = SvgIconNoChildrenProps; + +export const AuraWordmarkIcon = forwardRef< + SVGSVGElement, + AuraWordmarkIconProps +>((props, ref) => { + return ( + + <> + + + + ); +}); + +AuraWordmarkIcon.displayName = "AuraWordmarkIcon"; diff --git a/packages/odyssey-react-mui/src/icons.generated/index.ts b/packages/odyssey-react-mui/src/icons.generated/index.ts index c68d444c42..e93ffa8082 100644 --- a/packages/odyssey-react-mui/src/icons.generated/index.ts +++ b/packages/odyssey-react-mui/src/icons.generated/index.ts @@ -26,6 +26,8 @@ export * from "./ArrowUnsorted"; export * from "./ArrowUp"; export * from "./ArrowUpperLeft"; export * from "./ArrowUpperRight"; +export * from "./Aura"; +export * from "./AuraWordmark"; export * from "./Bug"; export * from "./Calendar"; export * from "./Call"; diff --git a/packages/odyssey-react-mui/src/labs/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav.tsx new file mode 100644 index 0000000000..425142dfeb --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/TopNav.tsx @@ -0,0 +1,430 @@ +/*! + * 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 styled from "@emotion/styled"; +import { memo, useMemo, ReactElement, useCallback, KeyboardEvent } from "react"; + +import { Box } from "../Box"; +import type { HtmlProps } from "../HtmlProps"; +import { + AuraIcon, + AuraWordmarkIcon, + QuestionCircleIcon, + SettingsIcon, +} from "../icons.generated"; +import { Link } from "../Link"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../OdysseyDesignTokensContext"; + +export type TopNavLinkItem = { + id: string; + label: string; + /** + * link added to the nav item. if it is undefined, static text will be displayed. + * fires onClick event when it is passed + */ + href?: string; + /** + * determines whether the link item is diabled + */ + isDisabled?: boolean; + /** + * Event fired when the nav item is clicked + */ + onClick?(): void; + /** + * The link target prop. e.g., "_blank" + */ + target?: string; +}; + +export type UserProfileProps = { + /** + * Logged in user profile icon to be displayed in the top nav + */ + profileIcon?: ReactElement; + /** + * Logged in user info to be displayed in the top nav + */ + userName: string; + /** + * Org name of the logged in user + */ + orgName: string; +}; + +export type TopNavProps = { + /** + * Determines whether to display the logo (aura & wordmark) + */ + hasLogo?: boolean; + /** + * Determines whether the search box is displayed + */ + search?: ReactElement; + /** + * Nav links in the top nav + */ + topNavLinkItems: TopNavLinkItem[]; + /** + * Displays an additional button after the link items if it is passed + */ + additionalNavItem?: ReactElement; + /** + * link to settings page + */ + settingsLink?: string; + /** + * help link + */ + helpLink?: string; + /** + * determines whether the divider is displayed before the user profile / account info + */ + hasDivider?: boolean; + + /** + * help link + */ + userProfile?: UserProfileProps; +} & Pick; + +const UserProfileContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + display: "flex", + alignItems: "center", + paddingRight: odysseyDesignTokens.Spacing4, +})); + +const UserProfile = ({ profileIcon, userName, orgName }: UserProfileProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + const userProfileIconStyles = useMemo( + () => ({ + paddingRight: odysseyDesignTokens.Spacing2, + }), + [odysseyDesignTokens], + ); + + const userProfileInfoContainerStyles = useMemo( + () => ({ + display: "flex", + flexDirection: "column", + }), + [], + ); + + const userProfileEmailInfoStyles = useMemo( + () => ({ + color: odysseyDesignTokens.TypographyColorHeading, + fontSize: odysseyDesignTokens.TypographySizeSubordinate, + }), + [odysseyDesignTokens], + ); + + const userProfileOrgInfoStyles = useMemo( + () => ({ + color: odysseyDesignTokens.TypographyColorSubordinate, + fontSize: odysseyDesignTokens.TypographySizeSubordinate, + }), + [odysseyDesignTokens], + ); + + return ( + + {profileIcon && {profileIcon}} + + {userName} + {orgName} + + + ); +}; + +const TopNavListContainer = styled.ul({ + padding: 0, + listStyle: "none", + listStyleType: "none", + display: "flex", + alignItems: "center", +}); + +const TopNavItemLabelContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})<{ + odysseyDesignTokens: DesignTokens; +}>(({ odysseyDesignTokens }) => ({ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + fontSize: odysseyDesignTokens.TypographyScale0, + fontWeight: odysseyDesignTokens.TypographyWeightHeading, +})); + +const TopNavListItemContainer = styled("li", { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isDisabled", +})<{ + odysseyDesignTokens: DesignTokens; + disabled?: boolean; + isDisabled?: boolean; +}>(({ odysseyDesignTokens, isDisabled }) => ({ + display: "flex", + alignItems: "center", + cursor: isDisabled ? "default" : "pointer", + pointerEvents: isDisabled ? "none" : "auto", + "& a": { + display: "flex", + alignItems: "center", + padding: `${odysseyDesignTokens.Spacing2} ${odysseyDesignTokens.Spacing4}`, + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + "& a:hover": { + textDecoration: "none", + backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit", + }, + "& div[role='button']:hover": { + backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit", + }, + "& a:focus-visible": { + outlineOffset: 0, + borderRadius: 0, + outlineWidth: odysseyDesignTokens.FocusOutlineWidthMain, + backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit", + }, +})); + +const TopNavItemContent = ({ + id, + label, + href, + target, + onClick, + isDisabled, +}: TopNavLinkItem) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + const NavItemContentClickContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", + })(() => ({ + display: "flex", + alignItems: "center", + width: "100%", + padding: `${odysseyDesignTokens.Spacing2} ${odysseyDesignTokens.Spacing4}`, + color: `${isDisabled ? odysseyDesignTokens.TypographyColorDisabled : odysseyDesignTokens.TypographyColorHeading} !important`, + "&:focus-visible": { + borderRadius: 0, + outlineColor: odysseyDesignTokens.FocusOutlineColorPrimary, + outlineStyle: odysseyDesignTokens.FocusOutlineStyle, + outlineWidth: odysseyDesignTokens.FocusOutlineWidthMain, + backgroundColor: odysseyDesignTokens.HueNeutral50, + textDecoration: "none", + }, + })); + + const TopNavItemContentKeyHandler = useCallback( + (event: KeyboardEvent) => { + if (event?.key === "Enter") { + event.preventDefault(); + event.stopPropagation(); + onClick?.(); + } + }, + [onClick], + ); + + const TopNavItemContent = useMemo(() => { + return ( + + { + // Use Link for nav items with links and div for disabled or non-link items + isDisabled ? ( + + + {label} + + + ) : !href ? ( + + + {label} + + + ) : ( + + + {label} + + + ) + } + + ); + }, [ + id, + label, + href, + target, + onClick, + isDisabled, + NavItemContentClickContainer, + TopNavItemContentKeyHandler, + odysseyDesignTokens, + ]); + + return TopNavItemContent; +}; + +const TopNav = ({ + hasLogo, + search, + topNavLinkItems, + additionalNavItem, + settingsLink, + helpLink, + hasDivider, + userProfile, +}: TopNavProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + const TopNavStyles = useMemo( + () => ({ + display: "flex", + alignItems: "center", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + height: odysseyDesignTokens.Spacing9, + }), + [odysseyDesignTokens], + ); + + const LogoContainerStyles = useMemo( + () => ({ + display: "flex", + alignItems: "center", + padding: `0 ${odysseyDesignTokens.Spacing9} 0 ${odysseyDesignTokens.Spacing5}`, + }), + [odysseyDesignTokens], + ); + + const LogoStyles = useMemo( + () => ({ + fontSize: odysseyDesignTokens.TypographyScale6, + }), + [odysseyDesignTokens], + ); + + const LogoWordmarkStyles = useMemo( + () => ({ + width: "55px", + height: "20px", + paddingLeft: odysseyDesignTokens.Spacing2, + }), + [odysseyDesignTokens], + ); + + const searchContainerStyles = useMemo( + () => ({ + width: "350px", + padding: `${odysseyDesignTokens.Spacing2} ${odysseyDesignTokens.Spacing3}`, + }), + [odysseyDesignTokens], + ); + + const additionalNavItemContainerStyles = useMemo( + () => ({ + marginLeft: "auto", + padding: `0 ${odysseyDesignTokens.Spacing3}`, + }), + [odysseyDesignTokens], + ); + + const linkContainerStyles = useMemo( + () => ({ + paddingRight: odysseyDesignTokens.Spacing3, + "& a": { + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + }, + }), + [odysseyDesignTokens], + ); + + const dividerContainerStyles = useMemo( + () => ({ + margin: `0 ${odysseyDesignTokens.Spacing3} 0 0`, + padding: `${odysseyDesignTokens.Spacing4} 0`, + borderLeft: `${odysseyDesignTokens.BorderWidthMain} solid ${odysseyDesignTokens.HueNeutral200}`, + }), + [odysseyDesignTokens], + ); + + return ( + + {hasLogo && ( + + + + + )} + {search && {search}} + + {topNavLinkItems?.map((item) => { + return ; + })} + + {additionalNavItem && ( + {additionalNavItem} + )} + {settingsLink && ( + + + + + + )} + {helpLink && ( + + + + + + )} + {hasDivider && } + {userProfile && UserProfile(userProfile)} + + ); +}; + +const MemoizedTopNav = memo(TopNav); +MemoizedTopNav.displayName = "TopNav"; + +export { MemoizedTopNav as TopNav }; diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts index 5101a6c3d7..71f87667e6 100644 --- a/packages/odyssey-react-mui/src/labs/index.ts +++ b/packages/odyssey-react-mui/src/labs/index.ts @@ -37,3 +37,4 @@ export * from "./Switch"; export * from "./NavAccordion"; export * from "./SideNav"; +export * from "./TopNav"; diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/TopNav/TopNav.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/TopNav/TopNav.stories.tsx new file mode 100644 index 0000000000..cbd1a79a15 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-labs/TopNav/TopNav.stories.tsx @@ -0,0 +1,149 @@ +/*! + * 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 { TopNav, TopNavProps } from "@okta/odyssey-react-mui/labs"; +import { Meta, StoryObj } from "@storybook/react"; +import { MuiThemeDecorator } from "../../../../.storybook/components"; +import { Button, SearchField } from "@okta/odyssey-react-mui"; +import { UserIcon } from "@okta/odyssey-react-mui/icons"; + +const storybookMeta: Meta = { + title: "Labs Components/TopNav", + component: TopNav, + argTypes: { + hasLogo: { + control: "boolean", + description: "Show Okta Logo", + table: { + type: { + summary: "boolean", + }, + }, + }, + search: { + control: "ReactElement", + description: "Display global search field", + table: { + type: { + summary: "ReactElement (SearchField)", + }, + }, + }, + topNavLinkItems: { + description: "Array of links to be displayed in the top nav", + table: { + type: { + summary: "Array", + }, + }, + }, + additionalNavItem: { + description: + "Additional element to be displayed at the end of the top nav", + table: { + type: { + summary: "ReactElement (Button)", + }, + }, + }, + settingsLink: { + description: "Display the settings icon/link", + table: { + type: { + summary: "boolean", + }, + }, + }, + helpLink: { + description: "Display the help icon/link", + table: { + type: { + summary: "boolean", + }, + }, + }, + hasDivider: { + description: + "Shows the divider after the nav items and before the user account info", + table: { + type: { + summary: "boolean", + }, + }, + }, + userProfile: { + description: "Shows the logged in user account info", + table: { + type: { + summary: "UserProfileProps", + }, + }, + }, + }, + args: { + hasLogo: true, + search: , + topNavLinkItems: [ + { + id: "link-01", + label: "Home", + href: "/", + }, + { + id: "link-02", + label: "Flows", + href: "/", + }, + { + id: "link-03", + label: "Connections", + href: "/", + isDisabled: true, + }, + { + id: "link-04", + label: "Template", + onClick: () => {}, + }, + ], + additionalNavItem: