From 0521de3230cb1381b22c4dde72c139f64ba967b3 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Mon, 28 Oct 2024 20:12:55 -0400 Subject: [PATCH 01/41] refactor: alphabetize props --- .../src/labs/SideNav/SideNav.tsx | 10 ++-- .../src/labs/SideNav/types.ts | 56 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 15d9347b0e..ef86e4f44b 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -242,16 +242,16 @@ const getHasScrollableContent = (scrollableContainer: HTMLElement) => scrollableContainer.scrollHeight > scrollableContainer.clientHeight; const SideNav = ({ - navHeaderText, + expandedWidth = DEFAULT_SIDE_NAV_WIDTH, + footerComponent, + footerItems, isCollapsible, isCompact, + logo, + navHeaderText, onCollapse, onExpand, sideNavItems, - expandedWidth = DEFAULT_SIDE_NAV_WIDTH, - footerItems, - footerComponent, - logo, }: SideNavProps) => { const [isSideNavCollapsed, setSideNavCollapsed] = useState(false); const [isContentScrollable, setIsContentScrollable] = useState(false); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/types.ts b/packages/odyssey-react-mui/src/labs/SideNav/types.ts index 07d4a96640..8a0a2f2b99 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/types.ts +++ b/packages/odyssey-react-mui/src/labs/SideNav/types.ts @@ -16,9 +16,10 @@ import type { statusSeverityValues } from "../../Status"; export type SideNavProps = { /** - * Side Nav header text that is usually reserved to show the App name + * A CSS length string indicating the customizable expanded width of the SideNav container. + * (it will be smaller if isCollapsible and collapsed) */ - navHeaderText: string; + expandedWidth?: string; /** * Determines whether the side nav is collapsible */ @@ -27,6 +28,14 @@ export type SideNavProps = { * Determines whether the side nav items use compact layout */ isCompact?: boolean; + /** + * An optional logo to display in the header. If not provided, will default to the Okta logo + */ + logo?: ReactElement; + /** + * Side Nav header text that is usually reserved to show the App name + */ + navHeaderText: string; /** * Triggers when the side nav is collapsed */ @@ -39,46 +48,36 @@ export type SideNavProps = { * Nav items in the side nav */ sideNavItems: SideNavItem[]; - /** - * A CSS length string indicating the customizable expanded width of the SideNav container. - * (it will be smaller if isCollapsible and collapsed) - */ - expandedWidth?: string; - /** - * An optional logo to display in the header. If not provided, will default to the Okta logo - */ - logo?: ReactElement; } & ( | { - /** - * Footer items in the side nav - */ - footerItems?: SideNavFooterItem[]; /** * footerComponent cannot be used if footerItems are defined */ footerComponent?: never; - } - | { /** - * footerItems cannot be used if footerComponent is defined + * Footer items in the side nav */ - footerItems?: never; + footerItems?: SideNavFooterItem[]; + } + | { /** * The component to display as the footer; if present the `footerItems` are ignored and not rendered. */ footerComponent?: ReactElement; + /** + * footerItems cannot be used if footerComponent is defined + */ + footerItems?: never; } ) & Pick; export type SideNavItem = { - id: string; - label: string; /** * The icon element to display at the end of the Nav Item */ endIcon?: ReactElement; + id: string; /** * Whether the item is disabled. When set to true the nav item is set to Disabled color, * the link/item is not clickable, and item with children is not expandable. @@ -88,6 +87,7 @@ export type SideNavItem = { * Whether the item is active/selected */ isSelected?: boolean; + label: string; /** * Event fired when the nav item is clicked */ @@ -110,25 +110,25 @@ export type SideNavItem = { target?: string; } & ( | { + children?: never; + href?: never; + isDefaultExpanded?: never; + isExpanded?: never; /** * Determines if the side nav item is a section header */ isSectionHeader: true; - href?: never; - children?: never; - isDefaultExpanded?: never; - isExpanded?: never; } | { + children?: never; /** * link added to the nav item. if it is undefined, static text will be displayed. * fires onClick event when it is passed */ href?: string; - children?: never; - isSectionHeader?: never; isDefaultExpanded?: never; isExpanded?: never; + isSectionHeader?: never; } | { /** @@ -136,6 +136,7 @@ export type SideNavItem = { */ children?: Array>; endIcon?: never; + href?: never; /** * Whether the accordion (nav item with children) is expanded by default */ @@ -146,7 +147,6 @@ export type SideNavItem = { */ isExpanded?: boolean; isSectionHeader?: never; - href?: never; } ); From eb2e3c87656e4ae7da821e7e16692bf8158b72b7 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Mon, 28 Oct 2024 21:17:24 -0400 Subject: [PATCH 02/41] feat: add the loading state --- .../src/labs/SideNav/SideNav.tsx | 153 +++++++++++------- .../src/labs/SideNav/SideNavHeader.tsx | 18 ++- .../src/labs/SideNav/types.ts | 1 + .../odyssey-labs/SideNav/SideNav.stories.tsx | 34 ++++ 4 files changed, 141 insertions(+), 65 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index ef86e4f44b..422767239b 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -37,6 +37,7 @@ import { } from "./SideNavItemContent"; import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; +import { Skeleton } from "@mui/material"; export const DEFAULT_SIDE_NAV_WIDTH = "300px"; @@ -238,15 +239,40 @@ const SideNavFooterItemsContainer = styled("div", { }, })); +const LoadingItemContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + alignItems: "center", + display: "flex", + gap: odysseyDesignTokens.Spacing2, + paddingBlock: odysseyDesignTokens.Spacing2, + paddingInline: odysseyDesignTokens.Spacing4, +})); + const getHasScrollableContent = (scrollableContainer: HTMLElement) => scrollableContainer.scrollHeight > scrollableContainer.clientHeight; +const LoadingItem = () => { + const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); + return ( + + + + + ); +}; + const SideNav = ({ expandedWidth = DEFAULT_SIDE_NAV_WIDTH, footerComponent, footerItems, isCollapsible, isCompact, + isLoading, logo, navHeaderText, onCollapse, @@ -444,75 +470,80 @@ const SideNav = ({ hasContentScrolled={hasContentScrolled} > } navHeaderText={navHeaderText} /> - {processedSideNavItems?.map((item) => { - const { - id, - label, - isSectionHeader, - startIcon, - children, - isDefaultExpanded, - isDisabled, - isExpanded, - } = item; - - if (isSectionHeader) { - return ( - - {label} - - ); - } else if (children) { - return ( - - - - {children} - - - - ); - } else { - return ( - - - - ); - } - })} + {isLoading + ? [...Array(6)].map((_, index) => ) + : processedSideNavItems?.map((item) => { + const { + id, + label, + isSectionHeader, + startIcon, + children, + isDefaultExpanded, + isDisabled, + isExpanded, + } = item; + + if (isSectionHeader) { + return ( + + {label} + + ); + } else if (children) { + return ( + + + + {children} + + + + ); + } else { + return ( + + + + ); + } + })} - {(footerItems || footerComponent) && ( + {(footerItems || footerComponent) && !isLoading && ( prop !== "odysseyDesignTokens", @@ -45,14 +46,16 @@ const SideNavHeaderContainer = styled("div", { })); const SideNavHeader = ({ - navHeaderText, + isLoading, logo, -}: Pick): ReactNode => { + navHeaderText, +}: Pick): ReactNode => { const odysseyDesignTokens = useOdysseyDesignTokens(); const sideNavHeaderStyles = useMemo( () => ({ marginTop: odysseyDesignTokens.Spacing2, + width: "100%", }), [odysseyDesignTokens], ); @@ -65,11 +68,18 @@ const SideNavHeader = ({ }} > - {logo} + {/* The skeleton takes the hardcoded dimensions of the Okta logo */} + {isLoading ? ( + + ) : ( + logo + )} - {navHeaderText} + + {isLoading ? : navHeaderText} + diff --git a/packages/odyssey-react-mui/src/labs/SideNav/types.ts b/packages/odyssey-react-mui/src/labs/SideNav/types.ts index 8a0a2f2b99..9abcae82fe 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/types.ts +++ b/packages/odyssey-react-mui/src/labs/SideNav/types.ts @@ -28,6 +28,7 @@ export type SideNavProps = { * Determines whether the side nav items use compact layout */ isCompact?: boolean; + isLoading?: boolean; /** * An optional logo to display in the header. If not provided, will default to the Okta logo */ 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 eedd8dfff1..19a6440fef 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 @@ -76,6 +76,15 @@ const storybookMeta: Meta = { }, }, }, + isLoading: { + control: "boolean", + description: "Controls whether the side nav shows the skeleton loader.", + table: { + type: { + summary: "boolean", + }, + }, + }, logo: { description: "Logo to be displayed in the Nav Header", }, @@ -291,6 +300,7 @@ export const Default: StoryObj = { expandedWidth={props.expandedWidth} isCompact={props.isCompact} isCollapsible={props.isCollapsible} + isLoading={props.isLoading} onCollapse={props.onCollapse} onExpand={props.onExpand} sideNavItems={props.sideNavItems} @@ -349,3 +359,27 @@ export const Default: StoryObj = { }); }, }; + +export const Loading: StoryObj = { + args: { + isLoading: true, + }, + render: (props: SideNavProps) => { + return ( +
+ +
+ ); + }, +}; From ca28c6314b37ad11a9c28fb3667ce3a5d56846fc Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Tue, 29 Oct 2024 11:40:52 -0400 Subject: [PATCH 03/41] feat: add more a11y roles --- packages/odyssey-react-mui/src/Box.tsx | 6 ++++-- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 4 +++- .../src/labs/SideNav/SideNavFooterContent.tsx | 4 ++++ .../src/labs/SideNav/SideNavItemContent.tsx | 2 ++ .../src/properties/odyssey-react-mui.properties | 3 +++ 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/odyssey-react-mui/src/Box.tsx b/packages/odyssey-react-mui/src/Box.tsx index 39358e507b..2402b29f36 100644 --- a/packages/odyssey-react-mui/src/Box.tsx +++ b/packages/odyssey-react-mui/src/Box.tsx @@ -11,7 +11,7 @@ */ import { Box as MuiBox, BoxProps as MuiBoxProps } from "@mui/material"; -import { ReactNode, forwardRef, memo } from "react"; +import { AriaRole, ReactNode, forwardRef, memo } from "react"; import type { HtmlProps } from "./HtmlProps"; @@ -19,17 +19,19 @@ export type BoxProps = { children?: ReactNode; component?: MuiBoxProps["component"]; id?: MuiBoxProps["id"]; + role?: AriaRole; sx?: MuiBoxProps["sx"]; } & Pick; const Box = forwardRef( - ({ children, component, id, sx, testId, translate }, ref) => ( + ({ children, component, id, role, sx, testId, translate }, ref) => ( diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 422767239b..7f7189e13a 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -38,6 +38,7 @@ import { import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; import { Skeleton } from "@mui/material"; +import { t } from "i18next"; export const DEFAULT_SIDE_NAV_WIDTH = "300px"; @@ -457,7 +458,7 @@ const SideNav = ({ ); return ( - + {label} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx index c05a4c650b..be8057842d 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx @@ -15,6 +15,7 @@ import { useOdysseyDesignTokens } from "../../OdysseyDesignTokensContext"; import type { SideNavFooterItem } from "./types"; import { Box } from "../../Box"; import { Link } from "../../Link"; +import { t } from "i18next"; const SideNavFooterContent = ({ footerItems, @@ -26,6 +27,9 @@ const SideNavFooterContent = ({ const footerContent = useMemo(() => { return footerItems?.map((item, index) => ( { // Use Link for nav items with links and div for disabled or non-link items diff --git a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties index 56e753811f..b2abed3e80 100644 --- a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties +++ b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties @@ -1,3 +1,6 @@ +navigation.label = Main navigation +navigation.footer = Navigation secondary links + breadcrumbs.home.text = Home breadcrumbs.label.text = Breadcrumbs close.text = Close From 37e241e3b3aa25acf42fcd980b89bef04fd189da Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Wed, 30 Oct 2024 10:15:32 -0400 Subject: [PATCH 04/41] feat(odyssey-storybook): add SideNavToggleButton --- .../src/labs/SideNav/SideNavToggleButton.tsx | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx new file mode 100644 index 0000000000..046924069c --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx @@ -0,0 +1,243 @@ +/*! + * 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 { Button as MuiButton } from "@mui/material"; +import type { ButtonProps as MuiButtonProps } from "@mui/material"; +import { + HTMLAttributes, + memo, + useCallback, + useImperativeHandle, + useRef, +} from "react"; +import styled from "@emotion/styled"; + +import { FocusHandle } from "../../inputUtils"; +import { MuiPropsContext, MuiPropsContextType } from "../../MuiPropsContext"; +import { + DesignTokens, + useOdysseyDesignTokens, +} from "../../OdysseyDesignTokensContext"; +import { Tooltip } from "../../Tooltip"; + +const StyledToggleButton = styled(MuiButton, { + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", +})( + ({ + isSideNavCollapsed, + odysseyDesignTokens, + }: { + isSideNavCollapsed: boolean; + odysseyDesignTokens: DesignTokens; + }) => ({ + backgroundColor: "transparent", + position: "relative", + padding: odysseyDesignTokens.Spacing3, + height: "unset", + border: 0, + + "&:hover, &:focus": { + backgroundColor: "transparent", + + "#lineOne": { + animation: + "lineOne-animate-to-collapse 250ms cubic-bezier(0, 0, 0.2, 1)", + animationFillMode: "forwards", + "@keyframes lineOne-animate-to-collapse": { + "0%": { + transform: "translate3d(-50%, -50%, 0)", + }, + "50%": { + transform: "translate3d(-50%, -50%, 0) rotate(-90deg) scaleY(.75)", + }, + "100%": { + transform: "translate3d(-50%, -27%, 0) rotate(-45deg) scaleY(.75)", + }, + }, + }, + + "#lineTwo": { + animation: + "lineTwo-animate-to-collapse 250ms cubic-bezier(0, 0, 0.2, 1)", + animationFillMode: "forwards", + "@keyframes lineTwo-animate-to-collapse": { + "0%": { + transform: "translate3d(-50%, -50%, 0)", + }, + "50%": { + transform: "translate3d(-50%, -50%, 0) rotate(-90deg) scaleY(.75)", + }, + "100%": { + transform: "translate3d(-50%, -73%, 0) rotate(-135deg) scaleY(.75)", + }, + }, + }, + + ...(isSideNavCollapsed && { + "#lineOne": { + animation: + "lineOne-animate-to-expand 250ms cubic-bezier(0, 0, 0.2, 1)", + animationFillMode: "forwards", + "@keyframes lineOne-animate-to-expand": { + "0%": { + transform: "translate3d(-50%, -50%, 0)", + }, + "50%": { + transform: "translate3d(-50%, -50%, 0) rotate(90deg) scaleY(.75)", + }, + "100%": { + transform: + "translate3d(-50%, -73%, 0) rotate(135deg) scaleY(.75)", + }, + }, + }, + + "#lineTwo": { + animation: + "lineTwo-animate-to-expand 250ms cubic-bezier(0, 0, 0.2, 1)", + animationFillMode: "forwards", + "@keyframes lineTwo-animate-to-expand": { + "0%": { + transform: "translate3d(-50%, -50%, 0)", + }, + "50%": { + transform: "translate3d(-50%, -50%, 0) rotate(90deg) scaleY(.75)", + }, + "100%": { + transform: "translate3d(-50%, -27%, 0) rotate(45deg) scaleY(.75)", + }, + }, + }, + }), + }, + + span: { + position: "absolute", + top: "50%", + left: "50%", + width: "2px", + height: odysseyDesignTokens.Spacing4, + backgroundColor: odysseyDesignTokens.HueNeutral500, + transform: "translate3d(-50%, -50%, 0)", + transition: `transform 250ms`, + }, + }), +); + +export type SideNavToggleButtonProps = { + /** + * The ref forwarded to the Button + */ + buttonRef?: React.RefObject; + /** + * The `id` of the item this button controls + */ + ariaControls: string; + /** + * The ID of the Button + */ + id?: string; + isSideNavCollapsed: boolean; + tabIndex?: HTMLAttributes["tabIndex"]; + /** + * The click event handler for the Button + */ + onClick?: MuiButtonProps["onClick"]; + onKeyDown?: MuiButtonProps["onKeyDown"]; +}; + +const SideNavToggleButton = ({ + ariaControls, + buttonRef, + id, + isSideNavCollapsed, + onClick, + tabIndex, +}: SideNavToggleButtonProps) => { + const odysseyDesignTokens = useOdysseyDesignTokens(); + + const localButtonRef = useRef(null); + + useImperativeHandle( + buttonRef, + () => ({ + focus: () => { + localButtonRef.current?.focus(); + }, + }), + [], + ); + + const renderButton = useCallback( + (muiProps: MuiPropsContextType) => { + return ( + { + if (element) { + ( + localButtonRef as React.MutableRefObject + ).current = element; + //@ts-expect-error ref is not an optional prop on the props context type + muiProps?.ref?.(element); + } + }} + tabIndex={tabIndex} + variant="floating" + > + + + + ); + }, + [ + ariaControls, + id, + isSideNavCollapsed, + odysseyDesignTokens, + onClick, + tabIndex, + ], + ); + + return ( + + {renderButton} + + ); +}; + +const MemoizedSideNavToggleButton = memo(SideNavToggleButton); +MemoizedSideNavToggleButton.displayName = "SideNavToggleButton"; + +export { MemoizedSideNavToggleButton as SideNavToggleButton }; From 760055d8667d32bc35da8b92e78052e4d812c7b8 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Wed, 30 Oct 2024 10:16:49 -0400 Subject: [PATCH 05/41] feat(odyssey-storybook): refacor and use new toggle button --- .../src/labs/SideNav/SideNav.tsx | 353 ++++++++---------- 1 file changed, 162 insertions(+), 191 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 7f7189e13a..4cae6774c0 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -28,8 +28,6 @@ import { } from "../../OdysseyDesignTokensContext"; import type { SideNavProps } from "./types"; import { OktaLogo } from "./OktaLogo"; -import { HandleIcon } from "./HandleIcon"; -import { CollapseIcon } from "./CollapseIcon"; import { SideNavHeader } from "./SideNavHeader"; import { SideNavItemContent, @@ -37,6 +35,7 @@ import { } from "./SideNavItemContent"; import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; +import { SideNavToggleButton } from "./SideNavToggleButton"; import { Skeleton } from "@mui/material"; import { t } from "i18next"; @@ -52,38 +51,36 @@ const SideNavContainer = styled("div")(() => ({ overflow: "hidden", })); -const SideNavCollapsedContainer = styled("div", { +const CollapsibleContent = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", + prop !== "odysseyDesignTokens" && + prop !== "isSideNavCollapsed" && + prop !== "expandedWidth", })( ({ odysseyDesignTokens, isSideNavCollapsed, + expandedWidth, }: { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; + expandedWidth: string; }) => ({ + position: "relative", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + flexDirection: "column", display: "flex", - "&:before": { - height: "100%", - width: 0, - content: '""', - }, - "&:has(svg:hover)": { - "&:before": { - width: isSideNavCollapsed ? "8px" : 0, - transitionProperty: "width, background-color", - transitionDuration: odysseyDesignTokens.TransitionDurationMain, - transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, - backgroundColor: isSideNavCollapsed - ? odysseyDesignTokens.HueNeutral200 - : "transparent", - }, - }, + opacity: isSideNavCollapsed ? 0 : 1, + visibility: isSideNavCollapsed ? "hidden" : "visible", + width: isSideNavCollapsed ? 0 : expandedWidth, + minWidth: isSideNavCollapsed ? 0 : expandedWidth, + transitionProperty: "opacity", + transitionDuration: odysseyDesignTokens.TransitionDurationMain, + transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, }), ); -const ToggleSideNavHandleContainer = styled("div", { +const StyledSideNav = styled("nav", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", })( @@ -94,60 +91,44 @@ const ToggleSideNavHandleContainer = styled("div", { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; }) => ({ - height: 0, - cursor: "pointer", - marginTop: SIDENAV_COLLAPSE_ICON_POSITION, - "& svg:nth-of-type(2)": { + position: "relative", + display: "flex", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + + "&::after": { + position: "absolute", + top: 0, + right: 0, + height: "100%", + width: odysseyDesignTokens.Spacing2, + backgroundColor: odysseyDesignTokens.HueNeutral200, + content: "''", opacity: 0, - width: 0, - transform: isSideNavCollapsed ? "rotate(180deg)" : "rotate(0deg)", + transition: `opacity ${odysseyDesignTokens.TransitionDurationMain}, transform ${odysseyDesignTokens.TransitionDurationMain}`, + transform: `translateX(0)`, }, - "&:hover, &:focus-visible": { - "& svg:nth-of-type(2)": { - opacity: 1, - width: "32px", - padding: odysseyDesignTokens.Spacing2, - transitionProperty: "opacity", - transitionDuration: odysseyDesignTokens.TransitionDurationMain, - transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, - }, - "& svg:nth-of-type(1)": { - opacity: 0, - width: 0, - transitionProperty: "opacity", - transitionDuration: odysseyDesignTokens.TransitionDurationMain, - transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, + + "&:has([data-sidenav-toggle='true']:hover), &:has([data-sidenav-toggle='true']:focus)": + { + ...(isSideNavCollapsed && { + "&::after": { + opacity: 1, + transform: `translateX(100%)`, + }, + + "[data-sidenav-toggle='true']": { + transform: `translate3d(calc(100% + ${odysseyDesignTokens.Spacing3}), 0, 0)`, + }, + }), }, - }, - }), -); -const SideNavExpandContainer = styled("nav", { - shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && - prop !== "isSideNavCollapsed" && - prop !== "expandedWidth", -})( - ({ - odysseyDesignTokens, - isSideNavCollapsed, - expandedWidth, - }: { - odysseyDesignTokens: DesignTokens; - isSideNavCollapsed: boolean; - expandedWidth: string; - }) => ({ - backgroundColor: odysseyDesignTokens.HueNeutralWhite, - flexDirection: "column", - display: "flex", - opacity: isSideNavCollapsed ? 0 : 1, - visibility: isSideNavCollapsed ? "hidden" : "visible", - width: isSideNavCollapsed ? 0 : expandedWidth, - minWidth: isSideNavCollapsed ? 0 : expandedWidth, - transitionProperty: "opacity", - transitionDuration: odysseyDesignTokens.TransitionDurationMain, - transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, - borderRight: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral50}`, + "[data-sidenav-toggle='true']": { + position: "absolute", + top: SIDENAV_COLLAPSE_ICON_POSITION, + right: 0, + transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`, + transform: `translate3d(calc(100% + ${odysseyDesignTokens.Spacing1}), 0, 0)`, + }, }), ); @@ -448,7 +429,7 @@ const SideNav = ({ }, [isSideNavCollapsed, setSideNavCollapsed, onExpand, onCollapse]); const sideNavExpandKeyHandler = useCallback( - (event: KeyboardEvent) => { + (event: KeyboardEvent) => { if (event?.key === "Enter" || event?.code === "Space") { event.preventDefault(); sideNavExpandClickHandler(); @@ -458,133 +439,123 @@ const SideNav = ({ ); return ( - - + - - } - navHeaderText={navHeaderText} + {isCollapsible && ( + - - - - {isLoading - ? [...Array(6)].map((_, index) => ) - : processedSideNavItems?.map((item) => { - const { - id, - label, - isSectionHeader, - startIcon, - children, - isDefaultExpanded, - isDisabled, - isExpanded, - } = item; - - if (isSectionHeader) { - return ( - - {label} - - ); - } else if (children) { - return ( - - - - {children} - - - - ); - } else { - return ( - - - - ); - } - })} - - - {(footerItems || footerComponent) && !isLoading && ( - - {footerComponent} - {footerItems && !footerComponent && ( - - - - )} - )} - - {isCollapsible && ( - - - - - - - )} + } + navHeaderText={navHeaderText} + /> + + + + {isLoading + ? [...Array(6)].map((_, index) => ) + : processedSideNavItems?.map((item) => { + const { + id, + label, + isSectionHeader, + startIcon, + children, + isDefaultExpanded, + isDisabled, + isExpanded, + } = item; + + if (isSectionHeader) { + return ( + + {label} + + ); + } else if (children) { + return ( + + + + {children} + + + + ); + } else { + return ( + + + + ); + } + })} + + + {(footerItems || footerComponent) && !isLoading && ( + + {footerComponent} + {footerItems && !footerComponent && ( + + + + )} + + )} + + ); }; From 619e815c58f7e97f9ef367b4e599bdc503d45cf4 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 11:10:49 -0400 Subject: [PATCH 06/41] test: add unit tests for SideNav --- .../src/labs/SideNav/OktaLogo.tsx | 1 + .../src/labs/SideNav/SideNavTest.test.tsx | 276 ++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx diff --git a/packages/odyssey-react-mui/src/labs/SideNav/OktaLogo.tsx b/packages/odyssey-react-mui/src/labs/SideNav/OktaLogo.tsx index 2196b44fc1..fff50ebfc0 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/OktaLogo.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/OktaLogo.tsx @@ -21,6 +21,7 @@ const OktaLogo = () => { fill="none" xmlns="http://www.w3.org/2000/svg" > + Okta { + test("can show the default Okta logo", async () => { + render( + , + ); + + expect(screen.getByTitle("Okta")).toBeInTheDocument(); + }); + + test("can show a custom logo", async () => { + render( + + } + navHeaderText="Header text" + sideNavItems={[ + { + id: "item0", + href: "#", + label: "Users", + }, + ]} + />, + ); + + expect(screen.getByAltText("Custom logo")).toBeInTheDocument(); + }); + + test("can show header text", async () => { + render( + , + ); + + expect( + screen.getByText("Header text", { selector: "h6" }), + ).toBeInTheDocument(); + }); + + test("is collapsible", async () => { + const menuItemText = "Users"; + + render( + , + ); + + expect(screen.getByText(menuItemText)).toBeVisible(); + + const collapseButton = screen.getByLabelText("collapse side navigation"); + fireEvent.click(collapseButton); + + expect(screen.getByText(menuItemText)).not.toBeVisible; + + const expandButton = screen.getByLabelText("expand side navigation"); + fireEvent.click(expandButton); + + expect(screen.getByText(menuItemText)).toBeVisible(); + }); + + test("can fire onCollapse event", async () => { + const menuItemText = "Users"; + const mockOnCollapse = jest.fn(); + + render( + , + ); + + const collapseButton = screen.getByLabelText("collapse side navigation"); + fireEvent.click(collapseButton); + + expect(mockOnCollapse).toBeCalled(); + }); + + test("can fire onExpand event", async () => { + const menuItemText = "Users"; + const mockOnExpand = jest.fn(); + + render( + , + ); + + const collapseButton = screen.getByLabelText("collapse side navigation"); + fireEvent.click(collapseButton); + + const expandButton = screen.getByLabelText("expand side navigation"); + fireEvent.click(expandButton); + + expect(mockOnExpand).toBeCalled(); + }); + + test("shows loading skeleton state", async () => { + const menuItemText = "Menu item"; + + render( + , + ); + + expect(screen.queryByText(menuItemText)).not.toBeInTheDocument(); + }); + + test("shows footer links", async () => { + const footerItemLabel = "Footer item"; + render( + , + ); + + const footer = screen.getByRole("menubar"); + expect(within(footer).getByText(footerItemLabel)).toBeVisible(); + }); + + test("shows custom footer component", async () => { + const footerComponentText = "This is a custom footer component."; + const footerComponent =

{footerComponentText}

; + + render( + , + ); + + expect(screen.getByText(footerComponentText)).toBeVisible(); + }); +}); + +test("displays sidenav link", async () => { + const menuLinkText = "Link"; + const menuClickableText = "Clickable"; + const headingText = "Heading"; + const accordionOuter = "Accordion outside"; + const accordionInner = "Accordion inside"; + + render( + {}, + }, + { + id: "menuHeading", + label: headingText, + isSectionHeader: true, + }, + { + id: "menuLink", + href: "#", + label: menuLinkText, + }, + { + id: "accordionOuter", + label: accordionOuter, + children: [ + { + id: "accordionInner", + href: "#", + label: accordionInner, + }, + ], + }, + ]} + />, + ); + + expect(screen.getByRole("menuitem", { name: menuLinkText })).toBeVisible(); + expect(screen.getByRole("button", { name: menuClickableText })).toBeVisible(); + expect(screen.getByRole("heading", { name: headingText })).toBeVisible(); + + const accordion = screen.getByText(accordionOuter); + expect(screen.getByText(accordionInner)).not.toBeVisible(); + fireEvent.click(accordion); + expect(screen.getByText(accordionInner)).toBeVisible(); +}); From 673f8eb882303b8e930ff78a373028319fc9ade1 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 11:14:16 -0400 Subject: [PATCH 07/41] fix: add accessible heading role to sidenav --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 4cae6774c0..b96e55a417 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -493,6 +493,7 @@ const SideNav = ({ id={id} key={id} odysseyDesignTokens={odysseyDesignTokens} + role="heading" > {label} From c5f2d88b643a8717261302172bbe59a8abb06968 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 12:22:53 -0400 Subject: [PATCH 08/41] refactor: replace t with useTranslation in imports --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 3 ++- .../src/labs/SideNav/SideNavFooterContent.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index b96e55a417..2b1419da35 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -37,7 +37,7 @@ import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; import { SideNavToggleButton } from "./SideNavToggleButton"; import { Skeleton } from "@mui/material"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; export const DEFAULT_SIDE_NAV_WIDTH = "300px"; @@ -268,6 +268,7 @@ const SideNav = ({ const resizeObserverRef = useRef(null); const intersectionObserverRef = useRef(null); const odysseyDesignTokens: DesignTokens = useOdysseyDesignTokens(); + const { t } = useTranslation(); useEffect(() => { const updateIsContentScrollable = () => { diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx index be8057842d..3a5cd7c931 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx @@ -15,7 +15,7 @@ import { useOdysseyDesignTokens } from "../../OdysseyDesignTokensContext"; import type { SideNavFooterItem } from "./types"; import { Box } from "../../Box"; import { Link } from "../../Link"; -import { t } from "i18next"; +import { useTranslation } from "react-i18next"; const SideNavFooterContent = ({ footerItems, @@ -23,6 +23,7 @@ const SideNavFooterContent = ({ footerItems: SideNavFooterItem[]; }) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const { t } = useTranslation(); const footerContent = useMemo(() => { return footerItems?.map((item, index) => ( From 3225267a2165dad0cb37df798f34e9fbfd920046 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Wed, 30 Oct 2024 11:26:06 -0400 Subject: [PATCH 09/41] feat(odyssey-storybook): add translations for toggle button --- .../src/labs/SideNav/SideNavToggleButton.tsx | 28 +++++++++---------- .../properties/odyssey-react-mui.properties | 2 ++ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx index 046924069c..851e343e68 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx @@ -17,9 +17,11 @@ import { memo, useCallback, useImperativeHandle, + useMemo, useRef, } from "react"; import styled from "@emotion/styled"; +import { useTranslation } from "react-i18next"; import { FocusHandle } from "../../inputUtils"; import { MuiPropsContext, MuiPropsContextType } from "../../MuiPropsContext"; @@ -165,6 +167,7 @@ const SideNavToggleButton = ({ tabIndex, }: SideNavToggleButtonProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const { t } = useTranslation(); const localButtonRef = useRef(null); @@ -178,6 +181,14 @@ const SideNavToggleButton = ({ [], ); + const toggleLabel = useMemo( + () => + isSideNavCollapsed + ? t("sidenav.toggle.expand") + : t("sidenav.toggle.collapse"), + [isSideNavCollapsed, t], + ); + const renderButton = useCallback( (muiProps: MuiPropsContextType) => { return ( @@ -185,11 +196,7 @@ const SideNavToggleButton = ({ {...muiProps} aria-controls={ariaControls} aria-expanded={!isSideNavCollapsed} - aria-label={ - isSideNavCollapsed - ? "expand side navigation" - : "collapse side navigation" - } + aria-label={toggleLabel} data-sidenav-toggle={true} id={id} isSideNavCollapsed={isSideNavCollapsed} @@ -219,19 +226,12 @@ const SideNavToggleButton = ({ odysseyDesignTokens, onClick, tabIndex, + toggleLabel, ], ); return ( - + {renderButton} ); diff --git a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties index b2abed3e80..ed67113aeb 100644 --- a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties +++ b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties @@ -85,6 +85,8 @@ severity.error = error severity.info = info severity.success = success severity.warning = warning +sidenav.toggle.expand = Expand side navigation +sidenav.toggle.collapse = Collapse side navigation switch.active = Active switch.inactive = Inactive table.columnvisibility.arialabel = Show/hide columns From 4431e6eaffaf59e10c7173db79b5c1a912b63a81 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Wed, 30 Oct 2024 12:32:04 -0400 Subject: [PATCH 10/41] feat(odyssey-storybook): update padding and cleanup footer code --- .../src/labs/SideNav/SideNav.tsx | 18 +++--- .../src/labs/SideNav/SideNavFooterContent.tsx | 64 ++++++++++--------- .../src/labs/SideNav/SideNavHeader.tsx | 38 ++++------- .../src/labs/SideNav/SideNavItemContent.tsx | 17 +++-- .../src/theme/components.tsx | 2 +- 5 files changed, 65 insertions(+), 74 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 2b1419da35..8765706015 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -31,7 +31,7 @@ import { OktaLogo } from "./OktaLogo"; import { SideNavHeader } from "./SideNavHeader"; import { SideNavItemContent, - SideNavListItemContainer, + StyledSideNavListItem, } from "./SideNavItemContent"; import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; @@ -170,9 +170,8 @@ const SectionHeader = styled("li", { fontSize: odysseyDesignTokens.TypographySizeOverline, fontWeight: odysseyDesignTokens.TypographyWeightHeadingBold, color: odysseyDesignTokens.HueNeutral600, - paddingTop: odysseyDesignTokens.Spacing3, - paddingBottom: odysseyDesignTokens.Spacing3, - paddingLeft: odysseyDesignTokens.Spacing4, + paddingBlock: odysseyDesignTokens.Spacing3, + paddingInline: odysseyDesignTokens.Spacing5, textTransform: "uppercase", })); @@ -189,7 +188,7 @@ const SideNavFooter = styled("div", { }) => ({ position: "sticky", bottom: 0, - paddingTop: odysseyDesignTokens.Spacing2, + paddingBlockStart: odysseyDesignTokens.Spacing2, transitionProperty: "box-shadow", transitionDuration: odysseyDesignTokens.TransitionDurationMain, transitionTiming: odysseyDesignTokens.TransitionTimingMain, @@ -203,8 +202,7 @@ const SideNavFooter = styled("div", { const SideNavFooterItemsContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - paddingTop: odysseyDesignTokens.Spacing2, - paddingBottom: odysseyDesignTokens.Spacing2, + paddingBlock: odysseyDesignTokens.Spacing2, display: "flex", justifyContent: "center", flexWrap: "wrap", @@ -228,7 +226,7 @@ const LoadingItemContainer = styled("div", { display: "flex", gap: odysseyDesignTokens.Spacing2, paddingBlock: odysseyDesignTokens.Spacing2, - paddingInline: odysseyDesignTokens.Spacing4, + paddingInline: odysseyDesignTokens.Spacing5, })); const getHasScrollableContent = (scrollableContainer: HTMLElement) => @@ -501,7 +499,7 @@ const SideNav = ({ ); } else if (children) { return ( - - + ); } else { return ( diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx index 3a5cd7c931..a202ab6a87 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx @@ -11,12 +11,31 @@ */ import { memo, useMemo } from "react"; -import { useOdysseyDesignTokens } from "../../OdysseyDesignTokensContext"; +import styled from "@emotion/styled"; + +import { + useOdysseyDesignTokens, + DesignTokens, +} from "../../OdysseyDesignTokensContext"; import type { SideNavFooterItem } from "./types"; import { Box } from "../../Box"; import { Link } from "../../Link"; import { useTranslation } from "react-i18next"; +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}`, + }, +})); + const SideNavFooterContent = ({ footerItems, }: { @@ -25,43 +44,26 @@ const SideNavFooterContent = ({ const odysseyDesignTokens = useOdysseyDesignTokens(); const { t } = useTranslation(); - const footerContent = useMemo(() => { - return footerItems?.map((item, index) => ( - { + return footerItems?.map((item) => ( + {item.href ? ( - - {item.label} - + {item.label} ) : ( - - {item.label} - - )} - {index < footerItems.length - 1 && ( - - | - + {item.label} )} - + )); }, [footerItems, odysseyDesignTokens]); - return footerContent; + return ( + + {memoizedFooterContent} + + ); }; const MemoizedSideNavFooterContent = memo(SideNavFooterContent); MemoizedSideNavFooterContent.displayName = "SideNavFooterContent"; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 67a06c8bb1..e50aff7733 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -11,7 +11,7 @@ */ import styled from "@emotion/styled"; -import { memo, ReactNode, useMemo } from "react"; +import { memo, ReactNode } from "react"; import { type DesignTokens, useOdysseyDesignTokens, @@ -19,18 +19,14 @@ import { import { Box } from "../../Box"; import { Heading6 } from "../../Typography"; import type { SideNavProps } from "./types"; -import { TOP_NAV_HEIGHT_TOKEN } from "../TopNav"; +// import { TOP_NAV_HEIGHT_TOKEN } from "../TopNav"; import { Skeleton } from "@mui/material"; const SideNavLogoContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - height: odysseyDesignTokens[TOP_NAV_HEIGHT_TOKEN], - padding: odysseyDesignTokens.Spacing3, - borderColor: odysseyDesignTokens.HueNeutral50, - borderStyle: odysseyDesignTokens.BorderStyleMain, - borderWidth: 0, - borderBottomWidth: odysseyDesignTokens.BorderWidthMain, + paddingInline: odysseyDesignTokens.Spacing5, + paddingBlock: odysseyDesignTokens.Spacing4, })); const SideNavHeaderContainer = styled("div", { @@ -39,10 +35,12 @@ const SideNavHeaderContainer = styled("div", { display: "flex", justifyContent: "space-between", alignItems: "center", - paddingLeft: odysseyDesignTokens.Spacing4, - paddingRight: odysseyDesignTokens.Spacing4, - paddingTop: odysseyDesignTokens.Spacing3, - paddingBottom: odysseyDesignTokens.Spacing3, + paddingInline: odysseyDesignTokens.Spacing5, + paddingBlock: odysseyDesignTokens.Spacing4, + + h2: { + margin: 0, + }, })); const SideNavHeader = ({ @@ -52,14 +50,6 @@ const SideNavHeader = ({ }: Pick): ReactNode => { const odysseyDesignTokens = useOdysseyDesignTokens(); - const sideNavHeaderStyles = useMemo( - () => ({ - marginTop: odysseyDesignTokens.Spacing2, - width: "100%", - }), - [odysseyDesignTokens], - ); - return ( - - - {isLoading ? : navHeaderText} - - + + {isLoading ? : navHeaderText} + ); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index 2188951a3d..ebe0696e81 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -32,7 +32,7 @@ import { } from "./SideNavItemContentContext"; import { ExternalLinkIcon } from "../../icons.generated"; -export const SideNavListItemContainer = styled("li", { +export const StyledSideNavListItem = styled("li", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isSelected", })<{ @@ -43,7 +43,6 @@ export const SideNavListItemContainer = styled("li", { display: "flex", alignItems: "center", backgroundColor: isSelected ? odysseyDesignTokens.HueNeutral50 : "unset", - margin: `${odysseyDesignTokens.Spacing1} 0`, "&:last-child": { marginBottom: odysseyDesignTokens.Spacing2, }, @@ -81,9 +80,13 @@ const GetNavItemContentStyles = ({ minHeight: contextValue.isCompact ? odysseyDesignTokens.Spacing6 : odysseyDesignTokens.Spacing7, - padding: contextValue.isCompact - ? `${odysseyDesignTokens.Spacing0} ${odysseyDesignTokens.Spacing4} ${odysseyDesignTokens.Spacing0} calc(${odysseyDesignTokens.Spacing4} * ${contextValue.depth})` - : `${odysseyDesignTokens.Spacing2} ${odysseyDesignTokens.Spacing4} ${odysseyDesignTokens.Spacing2} calc(${odysseyDesignTokens.Spacing4} * ${contextValue.depth})`, + paddingBlock: odysseyDesignTokens.Spacing2, + paddingInline: odysseyDesignTokens.Spacing5, + + ...(contextValue.isCompact && { + paddingBlock: odysseyDesignTokens.Spacing1, + }), + "&:focus-visible": { borderRadius: 0, outlineColor: odysseyDesignTokens.FocusOutlineColorPrimary, @@ -186,7 +189,7 @@ const SideNavItemContent = ({ ); return ( - ) } - + ); }; const MemoizedSideNavItemContent = memo(SideNavItemContent); diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index 4c29202952..6db1b9e016 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -138,7 +138,7 @@ export const components = ({ width: "1em", }, "&.nav-accordion-summary": { - padding: `${odysseyTokens.Spacing2} ${odysseyTokens.Spacing4}`, + padding: `${odysseyTokens.Spacing2} ${odysseyTokens.Spacing5}`, ".MuiAccordionSummary-expandIconWrapper.Mui-expanded": { transform: "rotate(-90deg) !important", }, From da6b506ab4ab5cf5f0583428d12f412ef6c6ca90 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 16:25:51 -0400 Subject: [PATCH 11/41] feat: add badge to SideNav --- .../src/labs/SideNav/SideNavItemContent.tsx | 5 + .../labs/SideNav/SideNavItemLinkContent.tsx | 14 +- .../src/labs/SideNav/SideNavTest.test.tsx | 374 ++++++++++-------- .../src/labs/SideNav/types.ts | 4 + .../odyssey-labs/SideNav/SideNav.stories.tsx | 8 + 5 files changed, 239 insertions(+), 166 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index ebe0696e81..75f2d87d0e 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -127,6 +127,7 @@ const NavItemLinkContainer = styled(NavItemLink, { })(GetNavItemContentStyles); const SideNavItemContent = ({ + count, id, label, href, @@ -141,6 +142,7 @@ const SideNavItemContent = ({ scrollRef, }: Pick< SideNavItem, + | "count" | "id" | "label" | "href" @@ -209,6 +211,7 @@ const SideNavItemContent = ({ isDisabled={isDisabled} > @@ -37,6 +38,7 @@ const SideNavItemLabelContainer = styled("div", { })); const SideNavItemLinkContent = ({ + count, label, startIcon, endIcon, @@ -44,12 +46,15 @@ const SideNavItemLinkContent = ({ statusLabel, }: Pick< SideNavItem, - "label" | "startIcon" | "endIcon" | "severity" | "statusLabel" + "count" | "label" | "startIcon" | "endIcon" | "severity" | "statusLabel" >): ReactNode => { const odysseyDesignTokens = useOdysseyDesignTokens(); const sideNavItemContentStyles = useMemo( () => ({ + alignItems: "center", + display: "flex", + gap: odysseyDesignTokens.Spacing1, marginLeft: odysseyDesignTokens.Spacing2, }), [odysseyDesignTokens], @@ -64,9 +69,12 @@ const SideNavItemLinkContent = ({ > {Boolean(startIcon)} {label} - {severity && ( + {(severity || count) && ( - + {count && } + {severity && ( + + )} )} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx index 3077a6481a..b131b7bccf 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx @@ -12,20 +12,23 @@ import { fireEvent, render, screen, within } from "@testing-library/react"; import { SideNav } from "./SideNav"; +import { OdysseyProvider } from "../../OdysseyProvider"; describe("SideNav", () => { test("can show the default Okta logo", async () => { render( - , + + + , ); expect(screen.getByTitle("Okta")).toBeInTheDocument(); @@ -33,43 +36,49 @@ describe("SideNav", () => { test("can show a custom logo", async () => { render( - - } - navHeaderText="Header text" - sideNavItems={[ - { - id: "item0", - href: "#", - label: "Users", - }, - ]} - />, + + + } + navHeaderText="Header text" + sideNavItems={[ + { + id: "item0", + href: "#", + label: "Users", + }, + ]} + /> + , ); expect(screen.getByAltText("Custom logo")).toBeInTheDocument(); }); test("can show header text", async () => { + const headerText = "Header text"; + render( - , + + + , ); expect( - screen.getByText("Header text", { selector: "h6" }), + screen.getByRole("heading", { name: headerText }), ).toBeInTheDocument(); }); @@ -77,27 +86,29 @@ describe("SideNav", () => { const menuItemText = "Users"; render( - , + + + , ); expect(screen.getByText(menuItemText)).toBeVisible(); - const collapseButton = screen.getByLabelText("collapse side navigation"); + const collapseButton = screen.getByLabelText("Collapse side navigation"); fireEvent.click(collapseButton); expect(screen.getByText(menuItemText)).not.toBeVisible; - const expandButton = screen.getByLabelText("expand side navigation"); + const expandButton = screen.getByLabelText("Expand side navigation"); fireEvent.click(expandButton); expect(screen.getByText(menuItemText)).toBeVisible(); @@ -108,21 +119,23 @@ describe("SideNav", () => { const mockOnCollapse = jest.fn(); render( - , + + + , ); - const collapseButton = screen.getByLabelText("collapse side navigation"); + const collapseButton = screen.getByLabelText("Collapse side navigation"); fireEvent.click(collapseButton); expect(mockOnCollapse).toBeCalled(); @@ -133,24 +146,26 @@ describe("SideNav", () => { const mockOnExpand = jest.fn(); render( - , + + + , ); - const collapseButton = screen.getByLabelText("collapse side navigation"); + const collapseButton = screen.getByLabelText("Collapse side navigation"); fireEvent.click(collapseButton); - const expandButton = screen.getByLabelText("expand side navigation"); + const expandButton = screen.getByLabelText("Expand side navigation"); fireEvent.click(expandButton); expect(mockOnExpand).toBeCalled(); @@ -160,17 +175,19 @@ describe("SideNav", () => { const menuItemText = "Menu item"; render( - , + + + , ); expect(screen.queryByText(menuItemText)).not.toBeInTheDocument(); @@ -179,23 +196,25 @@ describe("SideNav", () => { test("shows footer links", async () => { const footerItemLabel = "Footer item"; render( - , + + + , ); const footer = screen.getByRole("menubar"); @@ -207,70 +226,99 @@ describe("SideNav", () => { const footerComponent =

{footerComponentText}

; render( - , + + + , ); expect(screen.getByText(footerComponentText)).toBeVisible(); }); -}); -test("displays sidenav link", async () => { - const menuLinkText = "Link"; - const menuClickableText = "Clickable"; - const headingText = "Heading"; - const accordionOuter = "Accordion outside"; - const accordionInner = "Accordion inside"; - - render( - {}, - }, - { - id: "menuHeading", - label: headingText, - isSectionHeader: true, - }, - { - id: "menuLink", - href: "#", - label: menuLinkText, - }, - { - id: "accordionOuter", - label: accordionOuter, - children: [ + test("displays sidenav link", async () => { + const menuLinkText = "Link"; + const menuClickableText = "Clickable"; + const headingText = "Heading"; + const accordionOuter = "Accordion outside"; + const accordionInner = "Accordion inside"; + + render( + + {}, + }, + { + id: "menuHeading", + label: headingText, + isSectionHeader: true, + }, + { + id: "menuLink", + href: "#", + label: menuLinkText, + }, + { + id: "accordionOuter", + label: accordionOuter, + children: [ + { + id: "accordionInner", + href: "#", + label: accordionInner, + }, + ], + }, + ]} + /> + , + ); + + expect(screen.getByRole("menuitem", { name: menuLinkText })).toBeVisible(); + expect( + screen.getByRole("button", { name: menuClickableText }), + ).toBeVisible(); + expect(screen.getByRole("heading", { name: headingText })).toBeVisible(); + + const accordion = screen.getByText(accordionOuter); + expect(screen.getByText(accordionInner)).not.toBeVisible(); + fireEvent.click(accordion); + expect(screen.getByText(accordionInner)).toBeVisible(); + }); + + test("can show notification badge", async () => { + const menuItemText = "Menu item text"; + const badgeCount = 9; + + render( + + , - ); - - expect(screen.getByRole("menuitem", { name: menuLinkText })).toBeVisible(); - expect(screen.getByRole("button", { name: menuClickableText })).toBeVisible(); - expect(screen.getByRole("heading", { name: headingText })).toBeVisible(); - - const accordion = screen.getByText(accordionOuter); - expect(screen.getByText(accordionInner)).not.toBeVisible(); - fireEvent.click(accordion); - expect(screen.getByText(accordionInner)).toBeVisible(); + ]} + /> + , + ); + + expect(screen.getByRole("menuitem")).toHaveTextContent(`${badgeCount}`); + }); }); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/types.ts b/packages/odyssey-react-mui/src/labs/SideNav/types.ts index 9abcae82fe..1dd5dfd2eb 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/types.ts +++ b/packages/odyssey-react-mui/src/labs/SideNav/types.ts @@ -74,6 +74,10 @@ export type SideNavProps = { Pick; export type SideNavItem = { + /** + * The number to display as a count alongside the nav item + */ + count?: number; /** * The icon element to display at the end of the Nav 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 19a6440fef..72f1a9e4d3 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 @@ -26,6 +26,7 @@ import { DirectoryIcon, ServerIcon, FolderIcon, + NotificationIcon, } from "@okta/odyssey-react-mui/icons"; import { expect } from "@storybook/jest"; import { @@ -266,6 +267,13 @@ const storybookMeta: Meta = { label: "System Configuration", startIcon: , }, + { + id: "item6", + href: "/", + label: "Notifications", + startIcon: , + count: 1, + }, ], footerItems: [ { From 20b3c7f0b588834f5a4a8c78ca030a2359781765 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 16:57:47 -0400 Subject: [PATCH 12/41] refactor: updating accessibility roles --- .../odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 11 ++++------- .../src/labs/SideNav/SideNavFooterContent.tsx | 1 + .../src/labs/SideNav/SideNavItemContent.tsx | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 8765706015..a8ca35114d 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -152,10 +152,8 @@ const SideNavHeaderContainer = styled("div", { }), ); -const SideNavListContainer = styled("ul")(() => ({ +const SideNavListContainer = styled("div")(() => ({ padding: 0, - listStyle: "none", - listStyleType: "none", })); const SideNavScrollableContainer = styled("div")(() => ({ @@ -163,7 +161,7 @@ const SideNavScrollableContainer = styled("div")(() => ({ overflowY: "auto", })); -const SectionHeader = styled("li", { +const SectionHeader = styled("h3", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ fontFamily: odysseyDesignTokens.TypographyFamilyHeading, @@ -471,7 +469,7 @@ const SideNav = ({ /> - + {isLoading ? [...Array(6)].map((_, index) => ) : processedSideNavItems?.map((item) => { @@ -492,7 +490,6 @@ const SideNav = ({ id={id} key={id} odysseyDesignTokens={odysseyDesignTokens} - role="heading" > {label} @@ -514,7 +511,7 @@ const SideNav = ({ startIcon={startIcon} isDisabled={isDisabled} > - + {children} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx index a202ab6a87..2fbef7d2f6 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavFooterContent.tsx @@ -49,6 +49,7 @@ const SideNavFooterContent = ({ {item.href ? ( {item.label} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index 75f2d87d0e..465c26e8b3 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -32,7 +32,7 @@ import { } from "./SideNavItemContentContext"; import { ExternalLinkIcon } from "../../icons.generated"; -export const StyledSideNavListItem = styled("li", { +export const StyledSideNavListItem = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isSelected", })<{ @@ -224,7 +224,6 @@ const SideNavItemContent = ({ odysseyDesignTokens={odysseyDesignTokens} contextValue={contextValue} isDisabled={isDisabled} - role="button" tabIndex={0} onClick={onClick} onKeyDown={sideNavItemContentKeyHandler} From d65d3dc910430ed592285025f7370d81f98a930b Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Wed, 30 Oct 2024 17:21:56 -0500 Subject: [PATCH 13/41] fix: fixes Side nav collapse icon wasn't showing up over the app content --- .../src/labs/SideNav/SideNav.tsx | 29 +++++----- .../odyssey-labs/UiShell/UiShell.stories.tsx | 54 +++++++++++-------- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index d5d3816861..f076492196 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -45,13 +45,12 @@ export const DEFAULT_SIDE_NAV_WIDTH = "300px"; // to align it in the middle of the nav header text export const SIDENAV_COLLAPSE_ICON_POSITION = "77px"; -const SideNavContainer = styled("div")(() => ({ +const StyledSideNavContainer = styled("div")(() => ({ display: "flex", height: "100%", - overflow: "hidden", })); -const CollapsibleContent = styled("div", { +const StyledCollapsibleContent = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed" && @@ -91,21 +90,21 @@ const StyledSideNav = styled("nav", { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; }) => ({ - position: "relative", - display: "flex", backgroundColor: odysseyDesignTokens.HueNeutralWhite, + display: "flex", + position: "relative", "&::after": { - position: "absolute", - top: 0, - right: 0, - height: "100%", - width: odysseyDesignTokens.Spacing2, backgroundColor: odysseyDesignTokens.HueNeutral200, content: "''", + height: "100%", opacity: 0, - transition: `opacity ${odysseyDesignTokens.TransitionDurationMain}, transform ${odysseyDesignTokens.TransitionDurationMain}`, + position: "absolute", + right: 0, + top: 0, transform: `translateX(0)`, + transition: `opacity ${odysseyDesignTokens.TransitionDurationMain}, transform ${odysseyDesignTokens.TransitionDurationMain}`, + width: odysseyDesignTokens.Spacing2, }, "&:has([data-sidenav-toggle='true']:hover), &:has([data-sidenav-toggle='true']:focus)": @@ -440,7 +439,7 @@ const SideNav = ({ ); return ( - + )} - )} - + - + ); }; diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx index 81094727a9..c2247a182e 100644 --- a/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx @@ -13,12 +13,14 @@ import { Meta, StoryObj } from "@storybook/react"; import { MuiThemeDecorator } from "../../../../.storybook/components"; import { + PageTemplate, UiShell, UserProfile, type UiShellNavComponentProps, type UiShellProps, } from "@okta/odyssey-react-mui/labs"; import { + Banner, Button, OdysseyProvider, Paragraph, @@ -389,30 +391,38 @@ export const WithOdysseyAppContent: StoryObj = { args: { appComponent: ( - -
- - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris - lacinia leo quis sodales scelerisque. Maecenas tempor eget nunc - sit amet ultrices. Maecenas et varius ante. Nulla eu quam sit amet - orci fermentum dictum sit amet scelerisque libero. Proin luctus - semper elit, ut pretium massa tristique a. Mauris hendrerit ex eu - commodo egestas. Etiam a lacus aliquet, convallis metus et, - sollicitudin odio. Fusce vehicula purus sed orci elementum, ut - cursus diam sollicitudin. Pellentesque pulvinar nibh turpis, eu - finibus dolor egestas eget. Duis tellus mauris, pulvinar sit amet - ante a, aliquet laoreet sapien. Ut quis tempus massa. Fusce - fringilla mattis lacinia. Cras at pharetra quam, eu ultrices - ipsum. - -
-
-
-
+ + +
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris + lacinia leo quis sodales scelerisque. Maecenas tempor eget nunc + sit amet ultrices. Maecenas et varius ante. Nulla eu quam sit + amet orci fermentum dictum sit amet scelerisque libero. Proin + luctus semper elit, ut pretium massa tristique a. Mauris + hendrerit ex eu commodo egestas. Etiam a lacus aliquet, + convallis metus et, sollicitudin odio. Fusce vehicula purus sed + orci elementum, ut cursus diam sollicitudin. Pellentesque + pulvinar nibh turpis, eu finibus dolor egestas eget. Duis tellus + mauris, pulvinar sit amet ante a, aliquet laoreet sapien. Ut + quis tempus massa. Fusce fringilla mattis lacinia. Cras at + pharetra quam, eu ultrices ipsum. + +
+
+
+
+
), - optionalComponents: sharedOptionalComponents, + optionalComponents: { + ...sharedOptionalComponents, + banners: , + }, subscribeToPropChanges: (subscriber) => { subscriber({ sideNavProps: sharedSideNavProps, From 72dff57461255df89f1860c08ac13318d66ecbea Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Wed, 30 Oct 2024 22:19:43 -0400 Subject: [PATCH 14/41] fix: fix a11y role violations --- .../src/labs/SideNav/SideNav.tsx | 23 +++++++++++++------ .../src/labs/SideNav/SideNavItemContent.tsx | 1 - 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index f076492196..29653e5b60 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -162,6 +162,13 @@ const SideNavScrollableContainer = styled("div")(() => ({ overflowY: "auto", })); +const SectionHeaderContainer = styled("li", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + paddingBlock: odysseyDesignTokens.Spacing3, + paddingInline: odysseyDesignTokens.Spacing5, +})); + const SectionHeader = styled("h3", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ @@ -169,8 +176,6 @@ const SectionHeader = styled("h3", { fontSize: odysseyDesignTokens.TypographySizeOverline, fontWeight: odysseyDesignTokens.TypographyWeightHeadingBold, color: odysseyDesignTokens.HueNeutral600, - paddingBlock: odysseyDesignTokens.Spacing3, - paddingInline: odysseyDesignTokens.Spacing5, textTransform: "uppercase", })); @@ -474,7 +479,7 @@ const SideNav = ({ - + {isLoading ? [...Array(6)].map((_, index) => ) : processedSideNavItems?.map((item) => { @@ -491,13 +496,17 @@ const SideNav = ({ if (isSectionHeader) { return ( - - {label} - + + {label} + + ); } else if (children) { return ( @@ -516,7 +525,7 @@ const SideNav = ({ startIcon={startIcon} isDisabled={isDisabled} > - + {children} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index f8d4f263ad..cdf963b264 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -201,7 +201,6 @@ const SideNavItemContent = ({ aria-disabled={isDisabled} aria-current={isSelected ? "page" : undefined} odysseyDesignTokens={odysseyDesignTokens} - role="menuitem" > { // Use Link for nav items with links and div for disabled or non-link items From 5d291333dbc83c8264831b16dc1fa96509289467 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Thu, 31 Oct 2024 15:24:58 -0400 Subject: [PATCH 15/41] feat: add overflow scroll to TopNav --- .../src/labs/TopNav/TopNav.tsx | 23 +++++- .../src/labs/UiShell/UiShellContent.tsx | 77 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index cc7cde44dc..bd5ae07579 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -30,13 +30,19 @@ export type TopNavProps = { * React components that render into the right side of the top nav. */ rightSideComponent?: ReactElement; + /** + * Whether or not the underlying content has been scrolled + */ + isScrolled?: boolean; } & Pick; const StyledTopNavContainer = styled("div", { - shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", + shouldForwardProp: (prop) => + prop !== "odysseyDesignTokens" && prop !== "isScrolled", })<{ odysseyDesignTokens: DesignTokens; -}>(({ odysseyDesignTokens }) => ({ + isScrolled?: boolean; +}>(({ odysseyDesignTokens, isScrolled }) => ({ alignItems: "center", backgroundColor: odysseyDesignTokens.HueNeutral50, display: "flex", @@ -46,13 +52,22 @@ const StyledTopNavContainer = styled("div", { minHeight: TOP_NAV_HEIGHT, paddingBlock: odysseyDesignTokens.Spacing2, paddingInline: odysseyDesignTokens.Spacing6, + boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, + transition: `box-shadow ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`, })); -const TopNav = ({ leftSideComponent, rightSideComponent }: TopNavProps) => { +const TopNav = ({ + leftSideComponent, + rightSideComponent, + isScrolled, +}: TopNavProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( - + {leftSideComponent ??
} {rightSideComponent ??
} diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx index 6a799351d9..22a7995f2a 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx @@ -11,7 +11,14 @@ */ import styled from "@emotion/styled"; -import { memo, type ReactElement, type ReactNode } from "react"; +import { + memo, + useEffect, + useRef, + useState, + type ReactElement, + type ReactNode, +} from "react"; import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; import { SideNav, type SideNavProps } from "../SideNav"; @@ -103,6 +110,72 @@ const UiShellContent = ({ topNavProps, }: UiShellContentProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); + const [hasContentScrolled, setHasContentScrolled] = useState(false); + const scrollableContentRef = useRef(null); + const resizeObserverRef = useRef(null); + const scrollFrameRef = useRef(null); + const isScrolledRef = useRef(false); + + useEffect(() => { + const checkScrollPosition = () => { + if (scrollableContentRef.current) { + const isCurrentlyScrolled = scrollableContentRef.current.scrollTop > 0; + + // Only update state if the scrolled status has changed + if (isCurrentlyScrolled !== isScrolledRef.current) { + isScrolledRef.current = isCurrentlyScrolled; + setHasContentScrolled(isCurrentlyScrolled); + } + } + scrollFrameRef.current = null; + }; + + const handleScroll = () => { + // Only schedule a new frame if we don't already have one pending + if (!scrollFrameRef.current) { + scrollFrameRef.current = requestAnimationFrame(checkScrollPosition); + } + }; + + // If the window is resized, we may need to re-determine if the scrollable container has overflow + // Setup a ResizeObserver to know if the size of the scrollableContent changes + let resizeObserverDebounceTimer: ReturnType; + if (!resizeObserverRef.current) { + resizeObserverRef.current = new ResizeObserver(() => { + cancelAnimationFrame(resizeObserverDebounceTimer); + }); + } + + if (resizeObserverRef.current && scrollableContentRef.current) { + // Observe the container itself for size changes + resizeObserverRef.current.observe(scrollableContentRef.current); + } + + // Add scroll event listener to the container + if (scrollableContentRef.current) { + scrollableContentRef.current.addEventListener("scroll", handleScroll); + // Check initial scroll position + checkScrollPosition(); + } + + // Cleanup when unmounted: + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect(); + resizeObserverRef.current = null; + } + if (scrollableContentRef.current) { + scrollableContentRef.current.removeEventListener( + "scroll", + handleScroll, + ); + } + if (scrollFrameRef.current) { + cancelAnimationFrame(scrollFrameRef.current); + } + cancelAnimationFrame(resizeObserverDebounceTimer); + }; + }, []); return ( @@ -143,6 +216,7 @@ const UiShellContent = ({ @@ -151,6 +225,7 @@ const UiShellContent = ({ {optionalComponents?.banners} From b12e91daa63e281844b32db8fc8556cc3dd54d2f Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Thu, 31 Oct 2024 15:35:05 -0400 Subject: [PATCH 16/41] fix: adjust width of Heading skeleton --- .../odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index a78175ea32..889937708a 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -39,9 +39,11 @@ const SideNavHeaderContainer = styled("div", { alignItems: "center", paddingInline: odysseyDesignTokens.Spacing5, paddingBlock: odysseyDesignTokens.Spacing4, + width: "100%", h2: { margin: 0, + width: "100%", }, })); @@ -80,9 +82,7 @@ const SideNavHeader = ({ appName, isLoading, logo }: SideNavHeader) => { - - {isLoading ? : appName} - + {isLoading ? : appName} ); From d1004abe4bccea288466c19ddbb830d3d38f6b94 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Thu, 31 Oct 2024 15:19:09 -0400 Subject: [PATCH 17/41] feat(odyssey-storybook): add custom logo props --- .../src/labs/SideNav/SideNav.tsx | 26 ++++--- .../src/labs/SideNav/SideNavHeader.tsx | 43 ++++++----- .../src/labs/SideNav/SideNavItemContent.tsx | 76 +++++++++++++------ .../labs/SideNav/SideNavItemLinkContent.tsx | 6 +- .../src/labs/SideNav/SideNavLogo.tsx | 41 ++++++++++ .../src/labs/SideNav/types.ts | 75 +++++++++++------- .../src/labs/UiShell/UiShell.test.tsx | 3 +- .../src/labs/UiShell/UiShellContent.tsx | 13 +--- .../src/labs/UiShell/renderUiShell.test.tsx | 2 +- .../src/labs/UiShell/renderUiShell.tsx | 1 - .../src/theme/components.tsx | 14 +++- .../odyssey-labs/SideNav/SideNav.stories.tsx | 75 ++++++++++++------ 12 files changed, 245 insertions(+), 130 deletions(-) create mode 100644 packages/odyssey-react-mui/src/labs/SideNav/SideNavLogo.tsx diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 29653e5b60..773c9ba8e9 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -27,7 +27,6 @@ import { useOdysseyDesignTokens, } from "../../OdysseyDesignTokensContext"; import type { SideNavProps } from "./types"; -import { OktaLogo } from "./OktaLogo"; import { SideNavHeader } from "./SideNavHeader"; import { SideNavItemContent, @@ -90,9 +89,9 @@ const StyledSideNav = styled("nav", { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; }) => ({ - backgroundColor: odysseyDesignTokens.HueNeutralWhite, display: "flex", position: "relative", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, "&::after": { backgroundColor: odysseyDesignTokens.HueNeutral200, @@ -157,16 +156,19 @@ const SideNavListContainer = styled("ul")(() => ({ listStyleType: "none", })); -const SideNavScrollableContainer = styled("div")(() => ({ +const SideNavScrollableContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ flex: 1, overflowY: "auto", + paddingInline: odysseyDesignTokens.Spacing2, })); const SectionHeaderContainer = styled("li", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ paddingBlock: odysseyDesignTokens.Spacing3, - paddingInline: odysseyDesignTokens.Spacing5, + paddingInline: odysseyDesignTokens.Spacing4, })); const SectionHeader = styled("h3", { @@ -192,13 +194,13 @@ const SideNavFooter = styled("div", { }) => ({ position: "sticky", bottom: 0, - paddingBlockStart: odysseyDesignTokens.Spacing2, transitionProperty: "box-shadow", transitionDuration: odysseyDesignTokens.TransitionDurationMain, transitionTiming: odysseyDesignTokens.TransitionTimingMain, + backgroundColor: odysseyDesignTokens.HueNeutralWhite, // The box shadow should appear above the footer only if the scrollable region has overflow ...(isContentScrollable && { - boxShadow: odysseyDesignTokens.DepthHigh, + boxShadow: "0px -8px 8px -8px rgba(39, 39, 39, 0.08)", }), }), ); @@ -230,7 +232,7 @@ const LoadingItemContainer = styled("div", { display: "flex", gap: odysseyDesignTokens.Spacing2, paddingBlock: odysseyDesignTokens.Spacing2, - paddingInline: odysseyDesignTokens.Spacing5, + paddingInline: odysseyDesignTokens.Spacing4, })); const getHasScrollableContent = (scrollableContainer: HTMLElement) => @@ -252,15 +254,14 @@ const LoadingItem = () => { const SideNav = ({ appName, - customCompanyLogo, expandedWidth = DEFAULT_SIDE_NAV_WIDTH, footerComponent, footerItems, - hasCustomCompanyLogo, hasCustomFooter, isCollapsible, isCompact, isLoading, + logoProps, onCollapse, onExpand, sideNavItems, @@ -474,11 +475,14 @@ const SideNav = ({ } + logoProps={logoProps} /> - + {isLoading ? [...Array(6)].map((_, index) => ) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 889937708a..51a9600a86 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -11,24 +11,30 @@ */ import styled from "@emotion/styled"; -import { memo, type ReactElement } from "react"; +import { memo } from "react"; +import { Skeleton } from "@mui/material"; + +import { Box } from "../../Box"; import { type DesignTokens, useOdysseyDesignTokens, } from "../../OdysseyDesignTokensContext"; -import { Box } from "../../Box"; +import { SideNavLogo } from "./SideNavLogo"; +import { SideNavProps } from "./types"; import { Heading6 } from "../../Typography"; import { TOP_NAV_HEIGHT } from "../TopNav"; -import { Skeleton } from "@mui/material"; const SideNavLogoContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - height: "100%", - maxHeight: TOP_NAV_HEIGHT, - minHeight: TOP_NAV_HEIGHT, - paddingBlock: odysseyDesignTokens.Spacing4, - paddingInline: odysseyDesignTokens.Spacing5, + height: TOP_NAV_HEIGHT, + padding: odysseyDesignTokens.Spacing4, + + "svg, img": { + height: "100%", + width: "auto", + maxWidth: "100%", + }, })); const SideNavHeaderContainer = styled("div", { @@ -37,8 +43,7 @@ const SideNavHeaderContainer = styled("div", { display: "flex", justifyContent: "space-between", alignItems: "center", - paddingInline: odysseyDesignTokens.Spacing5, - paddingBlock: odysseyDesignTokens.Spacing4, + padding: odysseyDesignTokens.Spacing4, width: "100%", h2: { @@ -47,7 +52,7 @@ const SideNavHeaderContainer = styled("div", { }, })); -export type SideNavHeader = { +export type SideNavHeaderProps = { /** * The app's name. */ @@ -56,13 +61,13 @@ export type SideNavHeader = { * If the side nav currently has no items, it will be loading. */ isLoading?: boolean; - /** - * Company logo that displays above the app name. - */ - logo: ReactElement; -}; +} & Pick; -const SideNavHeader = ({ appName, isLoading, logo }: SideNavHeader) => { +const SideNavHeader = ({ + appName, + isLoading, + logoProps, +}: SideNavHeaderProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); return ( @@ -73,11 +78,11 @@ const SideNavHeader = ({ appName, isLoading, logo }: SideNavHeader) => { }} > - {/* The skeleton takes the hardcoded dimensions of the Okta logo */} {isLoading ? ( + // The skeleton takes the hardcoded dimensions of the Okta logo ) : ( - logo + )} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index cdf963b264..4603bc3f5e 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -40,9 +40,19 @@ export const StyledSideNavListItem = styled("li", { isSelected?: boolean; disabled?: boolean; }>(({ odysseyDesignTokens, isSelected }) => ({ - alignItems: "center", - backgroundColor: isSelected ? odysseyDesignTokens.HueNeutral50 : "unset", display: "flex", + alignItems: "center", + backgroundColor: "unset", + borderRadius: odysseyDesignTokens.BorderRadiusMain, + + ...(isSelected && { + color: `${odysseyDesignTokens.TypographyColorAction} !important`, + backgroundColor: odysseyDesignTokens.HueBlue50, + }), + + // "+ li": { + // marginBlockStart: odysseyDesignTokens.Spacing1 + // }, "&:last-child": { marginBottom: odysseyDesignTokens.Spacing2, @@ -67,48 +77,66 @@ const GetNavItemContentStyles = ({ odysseyDesignTokens, contextValue, isDisabled, + isSelected, }: { odysseyDesignTokens: DesignTokens; contextValue: SideNavItemContentContextValue; isDisabled?: boolean; + isSelected?: boolean; }) => { return { display: "flex", alignItems: "center", width: "100%", textDecoration: "none", - color: `${isDisabled ? odysseyDesignTokens.TypographyColorDisabled : odysseyDesignTokens.TypographyColorHeading} !important`, - minHeight: contextValue.isCompact - ? odysseyDesignTokens.Spacing6 - : odysseyDesignTokens.Spacing7, + color: `${odysseyDesignTokens.TypographyColorHeading} !important`, + minHeight: odysseyDesignTokens.Spacing7, paddingBlock: odysseyDesignTokens.Spacing2, - paddingInline: odysseyDesignTokens.Spacing5, + paddingInline: odysseyDesignTokens.Spacing4, + borderRadius: odysseyDesignTokens.BorderRadiusMain, + + ...(isSelected && { + color: `${odysseyDesignTokens.TypographyColorAction} !important`, + fontWeight: odysseyDesignTokens.TypographyWeightBodyBold, + }), + + ...(isDisabled && { + color: `${odysseyDesignTokens.TypographyColorDisabled} !important`, + }), ...(contextValue.isCompact && { paddingBlock: odysseyDesignTokens.Spacing1, + minHeight: odysseyDesignTokens.Spacing6, }), "&:focus-visible": { - borderRadius: 0, - outlineColor: odysseyDesignTokens.FocusOutlineColorPrimary, - outlineStyle: odysseyDesignTokens.FocusOutlineStyle, - outlineWidth: odysseyDesignTokens.FocusOutlineWidthMain, - textDecoration: "none", - outlineOffset: 0, - color: isDisabled - ? "default" - : `${odysseyDesignTokens.TypographyColorAction} !important`, - backgroundColor: !isDisabled - ? odysseyDesignTokens.HueNeutral50 - : "inherit", + // borderRadius: 0, + // outlineColor: odysseyDesignTokens.FocusOutlineColorPrimary, + // outlineStyle: odysseyDesignTokens.FocusOutlineStyle, + // outlineWidth: odysseyDesignTokens.FocusOutlineWidthMain, + outline: "none", + boxShadow: `inset 0 0 0 3px ${odysseyDesignTokens.PalettePrimaryMain}`, + // textDecoration: "none", + // outlineOffset: 0, + // color: isDisabled + // ? "default" + // : `${odysseyDesignTokens.TypographyColorAction} !important`, + // backgroundColor: !isDisabled + // ? odysseyDesignTokens.HueNeutral50 + // : "inherit", }, + "&:hover": { textDecoration: "none", cursor: isDisabled ? "default" : "pointer", - color: isDisabled - ? "default" - : `${odysseyDesignTokens.TypographyColorAction} !important`, - backgroundColor: !isDisabled ? odysseyDesignTokens.HueBlue50 : "inherit", + // color: `${odysseyDesignTokens.TypographyColorAction} !important`, + backgroundColor: !isDisabled + ? odysseyDesignTokens.HueNeutral50 + : "inherit", + + ...(isDisabled && { + color: "inherit", + }), }, }; }; @@ -209,6 +237,7 @@ const SideNavItemContent = ({ odysseyDesignTokens={odysseyDesignTokens} contextValue={contextValue} isDisabled={isDisabled} + isSelected={isSelected} > - {Boolean(startIcon)} {label} {(severity || count) && ( diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavLogo.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavLogo.tsx new file mode 100644 index 0000000000..f6983025df --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavLogo.tsx @@ -0,0 +1,41 @@ +/*! + * 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. + */ + +import { memo, useMemo } from "react"; +import { OktaLogo } from "./OktaLogo"; +import { SideNavLogoProps } from "./types"; + +const SideNavLogo = ({ + imageAltText, + href, + logoComponent, + imageUrl, +}: SideNavLogoProps) => { + const logo = useMemo(() => { + if (logoComponent) { + return logoComponent; + } + + if (imageAltText && imageUrl) { + return {imageAltText}; + } + + return ; + }, [imageAltText, logoComponent, imageUrl]); + + return href ? {logo} : logo; +}; + +const MemoizedSideNavLogo = memo(SideNavLogo); +MemoizedSideNavLogo.displayName = "SideNavLogo"; + +export { MemoizedSideNavLogo as SideNavLogo }; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/types.ts b/packages/odyssey-react-mui/src/labs/SideNav/types.ts index 037706662b..41c4b7c87a 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/types.ts +++ b/packages/odyssey-react-mui/src/labs/SideNav/types.ts @@ -14,6 +14,41 @@ import type { ReactElement } from "react"; import type { HtmlProps } from "../../HtmlProps"; import type { statusSeverityValues } from "../../Status"; +export type SideNavLogoProps = { + href?: string; +} & ( + | { + /** + * a component to render as the logo + */ + logoComponent: ReactElement; + imageAltText?: never; + imageUrl?: never; + } + | { + /** + * The src url to render in an `img` tag + */ + imageUrl: string; + /** + * alt text for the img logo + */ + imageAltText: string; + logoComponent?: never; + } + | { + /** + * The src url to render in an `img` tag + */ + imageUrl?: never; + /** + * alt text for the img logo + */ + imageAltText?: never; + logoComponent?: never; + } +); + export type SideNavProps = { /** * Side Nav header text that is usually reserved to show the App name @@ -37,9 +72,9 @@ export type SideNavProps = { */ isLoading?: boolean; /** - * An optional logo to display in the header. If not provided, will default to the Okta logo + * An optional logo component or src string for an img to display in the header. If not provided, will default to the Okta logo */ - logo?: ReactElement; + logoProps?: SideNavLogoProps; /** * Triggers when the side nav is collapsed */ @@ -55,37 +90,21 @@ export type SideNavProps = { } & ( | { /** - * An optional logo to display in the header. If not provided, will default to the Okta logo. - */ - customCompanyLogo: ReactElement; - /** - * Use the built-in Okta logo or a custom one. + * The component to display as the footer; if present the `footerItems` are ignored and not rendered. */ - hasCustomCompanyLogo: true; + footerComponent?: ReactElement; + footerItems?: never; + hasCustomFooter: true; } | { - customCompanyLogo?: never; - hasCustomCompanyLogo?: false; + footerComponent?: never; + /** + * Footer items in the side nav + */ + footerItems?: SideNavFooterItem[]; + hasCustomFooter?: false; } ) & - ( - | { - /** - * The component to display as the footer; if present the `footerItems` are ignored and not rendered. - */ - footerComponent?: ReactElement; - footerItems?: never; - hasCustomFooter: true; - } - | { - footerComponent?: never; - /** - * Footer items in the side nav - */ - footerItems?: SideNavFooterItem[]; - hasCustomFooter?: false; - } - ) & Pick; export type SideNavItem = { diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx index 40cb6177ba..9e4f8f9615 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx @@ -115,7 +115,7 @@ describe("UiShell", () => { test("renders optionally-available `componentSlots`", async () => { const optionalComponentTestIds: Array< keyof Required["optionalComponents"] - > = ["companyLogo", "sideNavFooter"]; + > = ["logoProps", "sideNavFooter"]; // This is the subscription we give the component, and then once subscribed, we're going to immediately call it with new props. const subscribeToPropChanges: UiShellProps["subscribeToPropChanges"] = ( @@ -125,7 +125,6 @@ describe("UiShell", () => { ...defaultComponentProps, sideNavProps: { appName: "", - hasCustomCompanyLogo: true, hasCustomFooter: true, sideNavItems: [], }, diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx index 22a7995f2a..3d79ec1071 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx @@ -67,7 +67,7 @@ export type UiShellNavComponentProps = { /** * Object that gets pass directly to the side nav component. */ - sideNavProps?: Omit; + sideNavProps?: Omit; /** * Object that gets pass directly to the top nav component. */ @@ -88,7 +88,6 @@ export type UiShellContentProps = { */ optionalComponents?: { banners?: ReactElement; - companyLogo?: SideNavProps["customCompanyLogo"]; sideNavFooter?: SideNavProps["footerComponent"]; topNavLeftSide?: TopNavProps["leftSideComponent"]; topNavRightSide?: TopNavProps["rightSideComponent"]; @@ -185,16 +184,6 @@ const UiShellContent = ({ { }); expect(slottedElements.banners).toBeInstanceOf(HTMLDivElement); - expect(slottedElements.companyLogo).toBeInstanceOf(HTMLDivElement); + expect(slottedElements.logoProps).toBeInstanceOf(HTMLDivElement); expect(slottedElements.sideNavFooter).toBeInstanceOf(HTMLDivElement); expect(slottedElements.topNavLeftSide).toBeInstanceOf(HTMLDivElement); expect(slottedElements.topNavRightSide).toBeInstanceOf(HTMLDivElement); diff --git a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx index 3e0002e029..080ce9c29b 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx @@ -24,7 +24,6 @@ export const optionalComponentSlotNames: Record< string > = { banners: "banners", - companyLogo: "company-logo", sideNavFooter: "side-nav-footer", topNavLeftSide: "top-nav-left-side", topNavRightSide: "top-nav-right-side", diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index 6db1b9e016..cae2546ead 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -138,7 +138,15 @@ export const components = ({ width: "1em", }, "&.nav-accordion-summary": { - padding: `${odysseyTokens.Spacing2} ${odysseyTokens.Spacing5}`, + borderRadius: odysseyTokens.BorderRadiusMain, + padding: `${odysseyTokens.Spacing2} ${odysseyTokens.Spacing4}`, + + "&:focus-visible": { + backgroundColor: "unset", + outline: "none", + boxShadow: `inset 0 0 0 3px ${odysseyTokens.PalettePrimaryMain}`, + }, + ".MuiAccordionSummary-expandIconWrapper.Mui-expanded": { transform: "rotate(-90deg) !important", }, @@ -160,9 +168,7 @@ export const components = ({ paddingInline: odysseyTokens.Spacing3, paddingBlock: odysseyTokens.Spacing4, "&.nav-accordion-details": { - paddingTop: 0, - paddingBottom: 0, - paddingLeft: odysseyTokens.Spacing2, + padding: 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 f3c767d5aa..d3341623ad 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 @@ -36,6 +36,7 @@ import { within, } from "@storybook/testing-library"; import { PlaywrightProps } from "../../odyssey-mui/storybookTypes"; +import PlaceholderLogo from "../PickerWithOptionAdornment/PlaceholderLogo"; const storybookMeta: Meta = { title: "Labs Components/SideNav", @@ -50,9 +51,6 @@ const storybookMeta: Meta = { }, }, }, - customCompanyLogo: { - description: "Logo to be displayed in the Nav Header.", - }, expandedWidth: { control: "text", description: "Width of the side nav in px", @@ -74,26 +72,16 @@ const storybookMeta: Meta = { }, }, }, - hasCustomCompanyLogo: { - control: "boolean", - description: - "Defines if a custom company logo should be visible when available.", - table: { - type: { - summary: "boolean", - }, - }, - }, - hasCustomFooter: { - control: "boolean", - description: - "Defines if a custom footer should be visible when available.", - table: { - type: { - summary: "boolean", - }, - }, - }, + // hasCustomFooter: { + // control: "boolean", + // description: + // "Defines if a custom footer should be visible when available.", + // table: { + // type: { + // summary: "boolean", + // }, + // }, + // }, isCollapsible: { control: "boolean", description: "Controls whether the side nav is collapsible", @@ -121,8 +109,14 @@ const storybookMeta: Meta = { }, }, }, - logo: { - description: "Logo to be displayed in the Nav Header", + logoProps: { + description: "Props passed in to render custom logo and/or link", + table: { + type: { + summary: + "href?: string; logoSrcUrl?: string; altText?: string; logoComponent?: ReactElement", + }, + }, }, onCollapse: { description: "Callback to be triggered when the side nav is collapsed", @@ -399,3 +393,34 @@ export const Loading: StoryObj = { ); }, }; + +export const CustomLogoElement: StoryObj = { + args: { + logoProps: { + logoComponent: , + }, + }, + render: (props) => { + return ( +
+ +
+ ); + }, +}; + +export const CustomLogoImage: StoryObj = { + args: { + logoProps: { + imageUrl: "https://placehold.co/600x60", + imageAltText: "My custom image logo", + }, + }, + render: (props) => { + return ( +
+ +
+ ); + }, +}; From 3eb6d35883311d81080ba8cfacd4e66d97015979 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 14:32:07 -0500 Subject: [PATCH 18/41] fix: moves banner above UI Shell # Conflicts: # packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx --- .../src/labs/UiShell/UiShellContent.tsx | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx index 3d79ec1071..f7ce216313 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx @@ -33,36 +33,44 @@ const StyledAppContainer = styled("div", { })<{ odysseyDesignTokens: DesignTokens; }>(({ odysseyDesignTokens }) => ({ + gridArea: "app-content", overflowX: "hidden", - overflowY: "scroll", + overflowY: "auto", paddingBlock: odysseyDesignTokens.Spacing5, paddingInline: odysseyDesignTokens.Spacing6, })); -const StyledFlexibleContentContainer = styled("div", { +const StyledBannersContainer = styled("div")(() => ({ + gridArea: "banners", +})); + +const StyledSideNavContainer = styled("div")(() => ({ + gridArea: "side-nav", +})); + +const StyledShellContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })<{ odysseyDesignTokens: DesignTokens; }>(({ odysseyDesignTokens }) => ({ backgroundColor: odysseyDesignTokens.HueNeutral50, - display: "flex", - flexBasis: "100%", - flexDirection: "column", - flexGrow: 1, -})); - -const StyledRigidContentContainer = styled("div")(() => ({ - flexShrink: 0, - height: "100%", -})); - -const StyledShellContainer = styled("div")(() => ({ - display: "flex", - flexWrap: "nowrap", + display: "grid", + gridGap: 0, + gridTemplateAreas: ` + "banners banners" + "side-nav top-nav" + "side-nav app-content" + `, + gridTemplateColumns: "auto 1fr", + gridTemplateRows: "auto auto 1fr", height: "100vh", width: "100vw", })); +const StyledTopNavContainer = styled("div")(() => ({ + gridArea: "top-nav", +})); + export type UiShellNavComponentProps = { /** * Object that gets pass directly to the side nav component. @@ -177,8 +185,12 @@ const UiShellContent = ({ }, []); return ( - - + + + {optionalComponents?.banners} + + + {sideNavProps && ( )} - + - + + - {optionalComponents?.banners} - {appComponent} From 8c4f60f17cb4208133820d6d81d0bf5ba39e637f Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 14:37:01 -0500 Subject: [PATCH 19/41] fix: fixes styling of TopNav slots and responsiveness # Conflicts: # packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx --- .../src/labs/TopNav/TopNav.tsx | 49 ++++++++++++------- .../src/labs/UiShell/UiShellContent.tsx | 15 +++--- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index bd5ae07579..078761d1a9 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -21,20 +21,13 @@ import { export const TOP_NAV_HEIGHT = `${64 / 14}rem`; -export type TopNavProps = { - /** - * React components that render into the left side of the top nav. - */ - leftSideComponent?: ReactElement; - /** - * React components that render into the right side of the top nav. - */ - rightSideComponent?: ReactElement; - /** - * Whether or not the underlying content has been scrolled - */ - isScrolled?: boolean; -} & Pick; +const StyledLeftSideContainer = styled("div")(() => ({ + flexGrow: 1, +})); + +const StyledRightSideContainer = styled("div")(() => ({ + flexShrink: 0, +})); const StyledTopNavContainer = styled("div", { shouldForwardProp: (prop) => @@ -45,21 +38,37 @@ const StyledTopNavContainer = styled("div", { }>(({ odysseyDesignTokens, isScrolled }) => ({ alignItems: "center", backgroundColor: odysseyDesignTokens.HueNeutral50, + boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, display: "flex", + gap: odysseyDesignTokens.Spacing4, height: "100%", justifyContent: "space-between", maxHeight: TOP_NAV_HEIGHT, minHeight: TOP_NAV_HEIGHT, paddingBlock: odysseyDesignTokens.Spacing2, paddingInline: odysseyDesignTokens.Spacing6, - boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, transition: `box-shadow ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`, })); +export type TopNavProps = { + /** + * Whether or not the underlying content has been scrolled + */ + isScrolled?: boolean; + /** + * React components that render into the left side of the top nav. + */ + leftSideComponent?: ReactElement; + /** + * React components that render into the right side of the top nav. + */ + rightSideComponent?: ReactElement; +} & Pick; + const TopNav = ({ + isScrolled, leftSideComponent, rightSideComponent, - isScrolled, }: TopNavProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); @@ -68,8 +77,12 @@ const TopNav = ({ odysseyDesignTokens={odysseyDesignTokens} isScrolled={isScrolled} > - {leftSideComponent ??
} - {rightSideComponent ??
} + + {leftSideComponent ??
} + + + {rightSideComponent ??
} + ); }; diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx index f7ce216313..a63c464f60 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx @@ -224,14 +224,13 @@ const UiShellContent = ({ - - {appComponent} - - + + {appComponent} + ); }; From 64fda9c60ecbbf3a8419ed5e90fd14bcdfcf6ce9 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 15:07:16 -0500 Subject: [PATCH 20/41] fix: refactors UI Shell scroll logic into useScrollState --- .../src/labs/UiShell/UiShellContent.tsx | 79 +------------------ .../src/labs/UiShell/useScrollState.ts | 56 +++++++++++++ 2 files changed, 60 insertions(+), 75 deletions(-) create mode 100644 packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx index a63c464f60..864eeed2f0 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShellContent.tsx @@ -11,14 +11,7 @@ */ import styled from "@emotion/styled"; -import { - memo, - useEffect, - useRef, - useState, - type ReactElement, - type ReactNode, -} from "react"; +import { memo, type ReactElement, type ReactNode } from "react"; import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; import { SideNav, type SideNavProps } from "../SideNav"; @@ -27,6 +20,7 @@ import { useOdysseyDesignTokens, type DesignTokens, } from "../../OdysseyDesignTokensContext"; +import { useScrollState } from "./useScrollState"; const StyledAppContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", @@ -117,72 +111,7 @@ const UiShellContent = ({ topNavProps, }: UiShellContentProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); - const [hasContentScrolled, setHasContentScrolled] = useState(false); - const scrollableContentRef = useRef(null); - const resizeObserverRef = useRef(null); - const scrollFrameRef = useRef(null); - const isScrolledRef = useRef(false); - - useEffect(() => { - const checkScrollPosition = () => { - if (scrollableContentRef.current) { - const isCurrentlyScrolled = scrollableContentRef.current.scrollTop > 0; - - // Only update state if the scrolled status has changed - if (isCurrentlyScrolled !== isScrolledRef.current) { - isScrolledRef.current = isCurrentlyScrolled; - setHasContentScrolled(isCurrentlyScrolled); - } - } - scrollFrameRef.current = null; - }; - - const handleScroll = () => { - // Only schedule a new frame if we don't already have one pending - if (!scrollFrameRef.current) { - scrollFrameRef.current = requestAnimationFrame(checkScrollPosition); - } - }; - - // If the window is resized, we may need to re-determine if the scrollable container has overflow - // Setup a ResizeObserver to know if the size of the scrollableContent changes - let resizeObserverDebounceTimer: ReturnType; - if (!resizeObserverRef.current) { - resizeObserverRef.current = new ResizeObserver(() => { - cancelAnimationFrame(resizeObserverDebounceTimer); - }); - } - - if (resizeObserverRef.current && scrollableContentRef.current) { - // Observe the container itself for size changes - resizeObserverRef.current.observe(scrollableContentRef.current); - } - - // Add scroll event listener to the container - if (scrollableContentRef.current) { - scrollableContentRef.current.addEventListener("scroll", handleScroll); - // Check initial scroll position - checkScrollPosition(); - } - - // Cleanup when unmounted: - return () => { - if (resizeObserverRef.current) { - resizeObserverRef.current.disconnect(); - resizeObserverRef.current = null; - } - if (scrollableContentRef.current) { - scrollableContentRef.current.removeEventListener( - "scroll", - handleScroll, - ); - } - if (scrollFrameRef.current) { - cancelAnimationFrame(scrollFrameRef.current); - } - cancelAnimationFrame(resizeObserverDebounceTimer); - }; - }, []); + const { isContentScrolled, scrollableContentRef } = useScrollState(); return ( @@ -217,7 +146,7 @@ const UiShellContent = ({ diff --git a/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts b/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts new file mode 100644 index 0000000000..254e017021 --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts @@ -0,0 +1,56 @@ +/*! + * 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. + */ + +import { useEffect, useRef, useState } from "react"; + +export const useScrollState = < + ScollingContentElement extends HTMLElement = HTMLDivElement, +>() => { + const [isContentScrolled, setIsContentScrolled] = useState(false); + + const scrollableContentRef = useRef(null); + + useEffect(() => { + if (scrollableContentRef.current) { + let requestedAnimationFrameId: number; + const scrollableContentElement = scrollableContentRef.current; + + const updateScrollState = () => { + cancelAnimationFrame(requestedAnimationFrameId); + + requestedAnimationFrameId = requestAnimationFrame(() => { + setIsContentScrolled(scrollableContentElement.scrollTop > 0); + }); + }; + + scrollableContentElement.addEventListener("scroll", updateScrollState); + + updateScrollState(); + + return () => { + scrollableContentElement.removeEventListener( + "scroll", + updateScrollState, + ); + + cancelAnimationFrame(requestedAnimationFrameId); + }; + } + + return () => {}; + }, []); + + return { + isContentScrolled, + scrollableContentRef, + }; +}; From 9076cf9ed19abdba66df7d7a67c45b3db79443b2 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 15:08:07 -0500 Subject: [PATCH 21/41] fix: minor rename of useScrollState generic --- packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts b/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts index 254e017021..c5c251ae76 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts +++ b/packages/odyssey-react-mui/src/labs/UiShell/useScrollState.ts @@ -13,11 +13,11 @@ import { useEffect, useRef, useState } from "react"; export const useScrollState = < - ScollingContentElement extends HTMLElement = HTMLDivElement, + ScrollableContentElement extends HTMLElement = HTMLDivElement, >() => { const [isContentScrolled, setIsContentScrolled] = useState(false); - const scrollableContentRef = useRef(null); + const scrollableContentRef = useRef(null); useEffect(() => { if (scrollableContentRef.current) { From 084f2005eb204e77d6b29dfc707e5b521938b9dc Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Thu, 31 Oct 2024 16:27:12 -0400 Subject: [PATCH 22/41] feat(odyssey-storybook): more style updates to match Figma --- .../src/labs/SideNav/NavAccordion.tsx | 38 +++++++++++-------- .../src/labs/SideNav/SideNavItemContent.tsx | 33 ++++++---------- .../src/theme/components.tsx | 14 ------- 3 files changed, 33 insertions(+), 52 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx b/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx index a3fff71264..108991dec8 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/NavAccordion.tsx @@ -20,7 +20,7 @@ import { import { ReactNode, memo } from "react"; import type { HtmlProps } from "../../HtmlProps"; -import { ChevronRightIcon } from "../../icons.generated"; +import { ChevronDownIcon } from "../../icons.generated"; import { DesignTokens, useOdysseyDesignTokens, @@ -86,26 +86,32 @@ const AccordionSummaryContainer = styled(MuiAccordionSummary, { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isCompact" && - prop != "isDisabled", + prop !== "isDisabled", })<{ odysseyDesignTokens: DesignTokens; isCompact?: boolean; isDisabled?: boolean; }>(({ odysseyDesignTokens, isCompact, isDisabled }) => ({ - minHeight: isCompact - ? `${odysseyDesignTokens.Spacing6}` - : `${odysseyDesignTokens.Spacing7}`, - padding: isCompact - ? `${odysseyDesignTokens.Spacing0} ${odysseyDesignTokens.Spacing4}` - : `${odysseyDesignTokens.Spacing2} ${odysseyDesignTokens.Spacing4}`, - "&:hover": { - backgroundColor: !isDisabled ? odysseyDesignTokens.HueBlue50 : "inherit", - "& span": { - color: isDisabled - ? "default" - : `${odysseyDesignTokens.TypographyColorAction}`, - }, + borderRadius: odysseyDesignTokens.BorderRadiusMain, + paddingBlock: odysseyDesignTokens.Spacing3, + paddingInline: odysseyDesignTokens.Spacing4, + lineHeight: 1.5, + + "&:focus-visible": { + backgroundColor: "unset", + outline: "none", + boxShadow: `inset 0 0 0 3px ${odysseyDesignTokens.PalettePrimaryMain}`, }, + + ...(isCompact && { + paddingBlock: odysseyDesignTokens.Spacing2, + }), + + ...(!isDisabled && { + "&:hover": { + backgroundColor: odysseyDesignTokens.HueNeutral50, + }, + }), })); const NavAccordion = ({ @@ -135,7 +141,7 @@ const NavAccordion = ({ } + expandIcon={} id={headerId} odysseyDesignTokens={odysseyDesignTokens} isCompact={isCompact} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index 4603bc3f5e..93facccc51 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -44,16 +44,14 @@ export const StyledSideNavListItem = styled("li", { alignItems: "center", backgroundColor: "unset", borderRadius: odysseyDesignTokens.BorderRadiusMain, + lineHeight: 1.5, + transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`, ...(isSelected && { color: `${odysseyDesignTokens.TypographyColorAction} !important`, backgroundColor: odysseyDesignTokens.HueBlue50, }), - // "+ li": { - // marginBlockStart: odysseyDesignTokens.Spacing1 - // }, - "&:last-child": { marginBottom: odysseyDesignTokens.Spacing2, }, @@ -91,9 +89,10 @@ const GetNavItemContentStyles = ({ textDecoration: "none", color: `${odysseyDesignTokens.TypographyColorHeading} !important`, minHeight: odysseyDesignTokens.Spacing7, - paddingBlock: odysseyDesignTokens.Spacing2, - paddingInline: odysseyDesignTokens.Spacing4, + paddingBlock: odysseyDesignTokens.Spacing4, + paddingInline: `calc(${odysseyDesignTokens.Spacing4} * ${contextValue.depth})`, borderRadius: odysseyDesignTokens.BorderRadiusMain, + transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`, ...(isSelected && { color: `${odysseyDesignTokens.TypographyColorAction} !important`, @@ -110,32 +109,20 @@ const GetNavItemContentStyles = ({ }), "&:focus-visible": { - // borderRadius: 0, - // outlineColor: odysseyDesignTokens.FocusOutlineColorPrimary, - // outlineStyle: odysseyDesignTokens.FocusOutlineStyle, - // outlineWidth: odysseyDesignTokens.FocusOutlineWidthMain, outline: "none", boxShadow: `inset 0 0 0 3px ${odysseyDesignTokens.PalettePrimaryMain}`, - // textDecoration: "none", - // outlineOffset: 0, - // color: isDisabled - // ? "default" - // : `${odysseyDesignTokens.TypographyColorAction} !important`, - // backgroundColor: !isDisabled - // ? odysseyDesignTokens.HueNeutral50 - // : "inherit", }, "&:hover": { textDecoration: "none", - cursor: isDisabled ? "default" : "pointer", - // color: `${odysseyDesignTokens.TypographyColorAction} !important`, + cursor: "pointer", backgroundColor: !isDisabled ? odysseyDesignTokens.HueNeutral50 : "inherit", ...(isDisabled && { color: "inherit", + cursor: "default", }), }, }; @@ -145,14 +132,16 @@ const NavItemContentContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop != "contextValue" && - prop !== "isDisabled", + prop !== "isDisabled" && + prop !== "isSelected", })(GetNavItemContentStyles); const NavItemLinkContainer = styled(NavItemLink, { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop != "contextValue" && - prop !== "isDisabled", + prop !== "isDisabled" && + prop !== "isSelected", })(GetNavItemContentStyles); const SideNavItemContent = ({ diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx index cae2546ead..6cd9ed02e5 100644 --- a/packages/odyssey-react-mui/src/theme/components.tsx +++ b/packages/odyssey-react-mui/src/theme/components.tsx @@ -137,20 +137,6 @@ export const components = ({ verticalAlign: "middle", width: "1em", }, - "&.nav-accordion-summary": { - borderRadius: odysseyTokens.BorderRadiusMain, - padding: `${odysseyTokens.Spacing2} ${odysseyTokens.Spacing4}`, - - "&:focus-visible": { - backgroundColor: "unset", - outline: "none", - boxShadow: `inset 0 0 0 3px ${odysseyTokens.PalettePrimaryMain}`, - }, - - ".MuiAccordionSummary-expandIconWrapper.Mui-expanded": { - transform: "rotate(-90deg) !important", - }, - }, }), content: () => ({ marginBlock: 0, From fd8550924b6955d25166b504f36b1724030f9852 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 15:40:21 -0500 Subject: [PATCH 23/41] feat: adds ability to know if UI Shell is rendered on the page --- .../src/labs/PageTemplate.tsx | 29 ++++++++++++------- .../src/labs/UiShell/index.ts | 2 +- .../src/labs/UiShell/renderUiShell.tsx | 5 ++++ .../src/labs/UiShell/useHasUiShell.ts | 25 ++++++++++++++++ .../odyssey-labs/UiShell/UiShell.stories.tsx | 4 +++ 5 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 packages/odyssey-react-mui/src/labs/UiShell/useHasUiShell.ts diff --git a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx index 2d3e345f77..fbe27b7d58 100644 --- a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx +++ b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx @@ -20,6 +20,7 @@ import { import { DocumentationIcon } from "../icons.generated"; import { Heading4, Subordinate } from "../Typography"; import { Link } from "../Link"; +import { useHasUiShell } from "./UiShell"; export type PageTemplateProps = { /** @@ -72,16 +73,21 @@ type TemplateContentProps = { const TemplateContainer = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && prop !== "isFullWidth", + prop !== "odysseyDesignTokens" && + prop !== "hasUiShell" && + prop !== "isFullWidth", })<{ - odysseyDesignTokens: DesignTokens; + hasUiShell: boolean; isFullWidth: boolean; -}>(({ odysseyDesignTokens, isFullWidth }) => ({ + odysseyDesignTokens: DesignTokens; +}>(({ hasUiShell, isFullWidth, odysseyDesignTokens }) => ({ maxWidth: isFullWidth ? "100%" : `calc(1440px + ${odysseyDesignTokens.Spacing6} + ${odysseyDesignTokens.Spacing6})`, - marginInline: isFullWidth ? odysseyDesignTokens.Spacing6 : "auto", - padding: odysseyDesignTokens.Spacing6, + marginInline: + isFullWidth && !hasUiShell ? odysseyDesignTokens.Spacing6 : "auto", + paddingBlock: odysseyDesignTokens.Spacing6, + paddingInline: hasUiShell ? 0 : odysseyDesignTokens.Spacing6, })); const TemplateHeader = styled("div")(() => ({ @@ -163,24 +169,27 @@ const TemplateContent = styled("div", { ); const PageTemplate = ({ - title, + children, description, documentationLink, documentationText, + drawer, + isFullWidth = false, primaryCallToActionComponent, secondaryCallToActionComponent, tertiaryCallToActionComponent, - children, - drawer, - isFullWidth = false, + title, }: PageTemplateProps) => { const odysseyDesignTokens = useOdysseyDesignTokens(); const { isOpen: isDrawerOpen, variant: drawerVariant } = drawer?.props ?? {}; + const hasUiShell = useHasUiShell(); + return ( diff --git a/packages/odyssey-react-mui/src/labs/UiShell/index.ts b/packages/odyssey-react-mui/src/labs/UiShell/index.ts index 098411173c..12f80169f0 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/index.ts +++ b/packages/odyssey-react-mui/src/labs/UiShell/index.ts @@ -11,7 +11,7 @@ */ export * from "./renderUiShell"; +export * from "./useHasUiShell"; export { UiShell, type UiShellProps } from "./UiShell"; - export { type UiShellNavComponentProps } from "./UiShellContent"; diff --git a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx index 080ce9c29b..85add6f84b 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.tsx @@ -19,6 +19,8 @@ import { UiShell, UiShellProps } from "./UiShell"; import { renderReactInWebComponent } from "../../web-component/renderReactInWebComponent"; import { type UiShellNavComponentProps } from "./UiShellContent"; +export const uiShellDataAttribute = "data-unified-ui-shell"; + export const optionalComponentSlotNames: Record< keyof Required["optionalComponents"], string @@ -59,6 +61,9 @@ export const renderUiShell = ({ const appRootElement = explicitAppRootElement || document.createElement("div"); + // Add this attribute so `PageTemplate` and potentially other components will know if they're in UI Shell with special padding already available. + uiShellRootElement.setAttribute(uiShellDataAttribute, ""); + const { publish: publishPropChanges, subscribe: subscribeToPropChanges } = createMessageBus>(); diff --git a/packages/odyssey-react-mui/src/labs/UiShell/useHasUiShell.ts b/packages/odyssey-react-mui/src/labs/UiShell/useHasUiShell.ts new file mode 100644 index 0000000000..ae5e852101 --- /dev/null +++ b/packages/odyssey-react-mui/src/labs/UiShell/useHasUiShell.ts @@ -0,0 +1,25 @@ +/*! + * 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. + */ + +import { useEffect, useState } from "react"; + +import { uiShellDataAttribute } from "./renderUiShell"; + +export const useHasUiShell = () => { + const [hasUiShell, setHasUiShell] = useState(false); + + useEffect(() => { + setHasUiShell(Boolean(document.querySelector(`[${uiShellDataAttribute}]`))); + }, []); + + return hasUiShell; +}; diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx index c2247a182e..4038ca2ed7 100644 --- a/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-labs/UiShell/UiShell.stories.tsx @@ -15,6 +15,7 @@ import { MuiThemeDecorator } from "../../../../.storybook/components"; import { PageTemplate, UiShell, + uiShellDataAttribute, UserProfile, type UiShellNavComponentProps, type UiShellProps, @@ -391,6 +392,9 @@ export const WithOdysseyAppContent: StoryObj = { args: { appComponent: ( + {/* This is normally rendered by `renderUiShell`, but we're rendering `UiShell` outside of a web component, so we need to add this data attribute ourselves. */} +
+ Date: Thu, 31 Oct 2024 16:14:20 -0500 Subject: [PATCH 24/41] fix: fixes PageTemplate padding for UI Shell mode --- packages/odyssey-react-mui/src/labs/PageTemplate.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx index fbe27b7d58..4ecb333f7d 100644 --- a/packages/odyssey-react-mui/src/labs/PageTemplate.tsx +++ b/packages/odyssey-react-mui/src/labs/PageTemplate.tsx @@ -86,8 +86,7 @@ const TemplateContainer = styled("div", { : `calc(1440px + ${odysseyDesignTokens.Spacing6} + ${odysseyDesignTokens.Spacing6})`, marginInline: isFullWidth && !hasUiShell ? odysseyDesignTokens.Spacing6 : "auto", - paddingBlock: odysseyDesignTokens.Spacing6, - paddingInline: hasUiShell ? 0 : odysseyDesignTokens.Spacing6, + padding: hasUiShell ? 0 : odysseyDesignTokens.Spacing6, })); const TemplateHeader = styled("div")(() => ({ From 33c455419f0de2d352032e660e0e6a2a04a6e536 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 16:15:09 -0500 Subject: [PATCH 25/41] fix: adds missing clip-path to TopNav shadow --- packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index 078761d1a9..c9141590dc 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -39,6 +39,7 @@ const StyledTopNavContainer = styled("div", { alignItems: "center", backgroundColor: odysseyDesignTokens.HueNeutral50, boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, + clipPath: "inset(0 0 -100% 0)", display: "flex", gap: odysseyDesignTokens.Spacing4, height: "100%", From d270745a39008e3ba6d6a76ab7fc20e8f8031d2b Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 16:34:27 -0500 Subject: [PATCH 26/41] fix: fixes border not showing up when scrolling side nav in UI Shell --- .../odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 773c9ba8e9..092f036d1b 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import styled from "@emotion/styled"; +import styled, { CSSObject } from "@emotion/styled"; import { memo, useMemo, @@ -144,9 +144,12 @@ const SideNavHeaderContainer = styled("div", { position: "sticky", top: 0, // The bottom border should appear only if the scrollable region has been scrolled - ...(hasContentScrolled && { - borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral50}`, - }), + ...(hasContentScrolled && + ({ + borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral50}`, + // boxShadow: true ? odysseyDesignTokens.DepthMedium : undefined, + // clipPath: "inset(0 0 -100% 0)", + } satisfies CSSObject)), }), ); @@ -355,7 +358,7 @@ const SideNav = ({ } cancelAnimationFrame(resizeObserverDebounceTimer); // Ensure timer is cleared on component unmount }; - }, []); + }, [sideNavItems]); const scrollIntoViewRef = useRef(null); /** From a4478031e5d970bc7eaaff8bf89cd5ca977dc202 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 16:36:08 -0500 Subject: [PATCH 27/41] fix: fixes topnav shadow not going over Surface --- packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index c9141590dc..45f49d8e87 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -49,6 +49,7 @@ const StyledTopNavContainer = styled("div", { paddingBlock: odysseyDesignTokens.Spacing2, paddingInline: odysseyDesignTokens.Spacing6, transition: `box-shadow ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`, + zIndex: 1, })); export type TopNavProps = { From 795b654de3db9ce0afe6ad1375486c180a6ef0b9 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Thu, 31 Oct 2024 17:01:47 -0500 Subject: [PATCH 28/41] fix: removes unused expandedWidth --- .../src/labs/SideNav/SideNav.tsx | 12 +++------- .../src/labs/SideNav/SideNavTest.test.tsx | 23 ++++++++++--------- .../src/labs/SideNav/types.ts | 5 ---- .../odyssey-labs/SideNav/SideNav.stories.tsx | 9 -------- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 092f036d1b..b3bd6aba05 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -51,18 +51,14 @@ const StyledSideNavContainer = styled("div")(() => ({ const StyledCollapsibleContent = styled("div", { shouldForwardProp: (prop) => - prop !== "odysseyDesignTokens" && - prop !== "isSideNavCollapsed" && - prop !== "expandedWidth", + prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", })( ({ odysseyDesignTokens, isSideNavCollapsed, - expandedWidth, }: { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; - expandedWidth: string; }) => ({ position: "relative", backgroundColor: odysseyDesignTokens.HueNeutralWhite, @@ -70,8 +66,8 @@ const StyledCollapsibleContent = styled("div", { display: "flex", opacity: isSideNavCollapsed ? 0 : 1, visibility: isSideNavCollapsed ? "hidden" : "visible", - width: isSideNavCollapsed ? 0 : expandedWidth, - minWidth: isSideNavCollapsed ? 0 : expandedWidth, + width: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, + minWidth: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, transitionProperty: "opacity", transitionDuration: odysseyDesignTokens.TransitionDurationMain, transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, @@ -257,7 +253,6 @@ const LoadingItem = () => { const SideNav = ({ appName, - expandedWidth = DEFAULT_SIDE_NAV_WIDTH, footerComponent, footerItems, hasCustomFooter, @@ -466,7 +461,6 @@ const SideNav = ({ { render( { alt="Custom logo" /> } - navHeaderText="Header text" + appName="Header text" sideNavItems={[ { id: "item0", @@ -65,7 +65,7 @@ describe("SideNav", () => { render( { { { { { render( { render( { render( { render( = { }, }, }, - expandedWidth: { - control: "text", - description: "Width of the side nav in px", - table: { - type: { - summary: "string", - }, - }, - }, footerComponent: { description: "Custom footer component to render in place of footer items.", From bb802d06d9ead8e55d42f8cf16dd7734fa4669c7 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 13:49:24 -0400 Subject: [PATCH 29/41] feat(odyssey-react-mui): updated footer padding --- .../src/labs/SideNav/SideNav.tsx | 21 ++++++------ .../src/labs/SideNav/SideNavItemContent.tsx | 32 +++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index b3bd6aba05..74f222c6e6 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -207,20 +207,23 @@ const SideNavFooter = styled("div", { const SideNavFooterItemsContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - paddingBlock: odysseyDesignTokens.Spacing2, + paddingBlockStart: odysseyDesignTokens.Spacing5, + paddingBlockEnd: odysseyDesignTokens.Spacing4, + paddingInline: odysseyDesignTokens.Spacing5, display: "flex", - justifyContent: "center", flexWrap: "wrap", alignItems: "center", fontSize: odysseyDesignTokens.TypographySizeOverline, - "& a": { + + a: { color: `${odysseyDesignTokens.TypographyColorHeading} !important`, - }, - "& a:hover": { - textDecoration: "none", - }, - "& a:visited": { - color: odysseyDesignTokens.TypographyColorHeading, + + "&:hover": { + textDecoration: "none", + }, + "&:visited": { + color: odysseyDesignTokens.TypographyColorHeading, + }, }, })); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx index 93facccc51..b27c6101eb 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemContent.tsx @@ -94,6 +94,25 @@ const GetNavItemContentStyles = ({ borderRadius: odysseyDesignTokens.BorderRadiusMain, transition: `backgroundColor ${odysseyDesignTokens.TransitionDurationMain}, color ${odysseyDesignTokens.TransitionDurationMain}`, + "&:hover": { + textDecoration: "none", + cursor: "pointer", + backgroundColor: !isDisabled + ? odysseyDesignTokens.HueNeutral50 + : "inherit", + + ...(isDisabled && { + color: "inherit", + cursor: "default", + }), + + ...(isSelected && { + "&:hover": { + backgroundColor: odysseyDesignTokens.HueBlue50, + }, + }), + }, + ...(isSelected && { color: `${odysseyDesignTokens.TypographyColorAction} !important`, fontWeight: odysseyDesignTokens.TypographyWeightBodyBold, @@ -112,19 +131,6 @@ const GetNavItemContentStyles = ({ outline: "none", boxShadow: `inset 0 0 0 3px ${odysseyDesignTokens.PalettePrimaryMain}`, }, - - "&:hover": { - textDecoration: "none", - cursor: "pointer", - backgroundColor: !isDisabled - ? odysseyDesignTokens.HueNeutral50 - : "inherit", - - ...(isDisabled && { - color: "inherit", - cursor: "default", - }), - }, }; }; From b696d5266d2d63c793bc6d9f460368d38681547f Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 14:27:19 -0400 Subject: [PATCH 30/41] feat(odyssey-react-mui): updated footer padding and logos dont grow --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 4 ++-- packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 74f222c6e6..06e03419f1 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -207,8 +207,8 @@ const SideNavFooter = styled("div", { const SideNavFooterItemsContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - paddingBlockStart: odysseyDesignTokens.Spacing5, - paddingBlockEnd: odysseyDesignTokens.Spacing4, + paddingBlock: odysseyDesignTokens.Spacing4, + // paddingBlockEnd: odysseyDesignTokens.Spacing4, paddingInline: odysseyDesignTokens.Spacing5, display: "flex", flexWrap: "wrap", diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 51a9600a86..4a2e0f6d3c 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -27,11 +27,13 @@ import { TOP_NAV_HEIGHT } from "../TopNav"; const SideNavLogoContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + display: "flex", + alignItems: "center", height: TOP_NAV_HEIGHT, padding: odysseyDesignTokens.Spacing4, "svg, img": { - height: "100%", + maxHeight: "100%", width: "auto", maxWidth: "100%", }, From 33acce7830e67a073a66b80f39639efe6e6993bf Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Fri, 1 Nov 2024 13:45:21 -0500 Subject: [PATCH 31/41] fix: fixes clip-path issue causing top nav to cut-off modals in Admin --- packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx index 45f49d8e87..f43cf1d78e 100644 --- a/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx +++ b/packages/odyssey-react-mui/src/labs/TopNav/TopNav.tsx @@ -39,7 +39,7 @@ const StyledTopNavContainer = styled("div", { alignItems: "center", backgroundColor: odysseyDesignTokens.HueNeutral50, boxShadow: isScrolled ? odysseyDesignTokens.DepthMedium : undefined, - clipPath: "inset(0 0 -100% 0)", + clipPath: "inset(0 0 -100vh 0)", display: "flex", gap: odysseyDesignTokens.Spacing4, height: "100%", From 58beae79f7edf70a433323717841d30efaee39ba Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Fri, 1 Nov 2024 13:46:13 -0500 Subject: [PATCH 32/41] fix: removes unused commented CSS from side nav --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 06e03419f1..a49022d069 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -143,8 +143,6 @@ const SideNavHeaderContainer = styled("div", { ...(hasContentScrolled && ({ borderBottom: `${odysseyDesignTokens.BorderWidthMain} ${odysseyDesignTokens.BorderStyleMain} ${odysseyDesignTokens.HueNeutral50}`, - // boxShadow: true ? odysseyDesignTokens.DepthMedium : undefined, - // clipPath: "inset(0 0 -100% 0)", } satisfies CSSObject)), }), ); From 4c709d91c258772746c9941d91c7a038a5103763 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 14:41:58 -0400 Subject: [PATCH 33/41] feat(odyssey-react-mui): use generic in callback for keyboard event --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index a49022d069..91b361c998 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -16,9 +16,9 @@ import { useMemo, useState, useCallback, - KeyboardEvent, useRef, useEffect, + KeyboardEventHandler, } from "react"; import { NavAccordion } from "./NavAccordion"; @@ -433,8 +433,10 @@ const SideNav = ({ setSideNavCollapsed(!isSideNavCollapsed); }, [isSideNavCollapsed, setSideNavCollapsed, onExpand, onCollapse]); - const sideNavExpandKeyHandler = useCallback( - (event: KeyboardEvent) => { + const sideNavExpandKeyHandler = useCallback< + KeyboardEventHandler + >( + (event) => { if (event?.key === "Enter" || event?.code === "Space") { event.preventDefault(); sideNavExpandClickHandler(); From 5d56b058554831c28f77759ee2a8e0a195d3dff8 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 16:26:24 -0400 Subject: [PATCH 34/41] feat(odyssey-react-mui): use grid for sidenav layout --- .../src/labs/SideNav/SideNav.tsx | 74 ++++++++----------- .../src/labs/SideNav/SideNavHeader.tsx | 26 ++++--- .../src/labs/SideNav/SideNavToggleButton.tsx | 2 +- .../properties/odyssey-react-mui.properties | 4 +- 4 files changed, 49 insertions(+), 57 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 91b361c998..ef187a13fc 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -20,12 +20,15 @@ import { useEffect, KeyboardEventHandler, } from "react"; +import { Skeleton } from "@mui/material"; +import { useTranslation } from "react-i18next"; import { NavAccordion } from "./NavAccordion"; import { DesignTokens, useOdysseyDesignTokens, } from "../../OdysseyDesignTokensContext"; +import { OdysseyThemeProvider } from "../../OdysseyThemeProvider"; import type { SideNavProps } from "./types"; import { SideNavHeader } from "./SideNavHeader"; import { @@ -35,8 +38,6 @@ import { import { SideNavFooterContent } from "./SideNavFooterContent"; import { SideNavItemContentContext } from "./SideNavItemContentContext"; import { SideNavToggleButton } from "./SideNavToggleButton"; -import { Skeleton } from "@mui/material"; -import { useTranslation } from "react-i18next"; export const DEFAULT_SIDE_NAV_WIDTH = "300px"; @@ -44,11 +45,6 @@ export const DEFAULT_SIDE_NAV_WIDTH = "300px"; // to align it in the middle of the nav header text export const SIDENAV_COLLAPSE_ICON_POSITION = "77px"; -const StyledSideNavContainer = styled("div")(() => ({ - display: "flex", - height: "100%", -})); - const StyledCollapsibleContent = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens" && prop !== "isSideNavCollapsed", @@ -61,16 +57,15 @@ const StyledCollapsibleContent = styled("div", { isSideNavCollapsed: boolean; }) => ({ position: "relative", - backgroundColor: odysseyDesignTokens.HueNeutralWhite, - flexDirection: "column", - display: "flex", - opacity: isSideNavCollapsed ? 0 : 1, - visibility: isSideNavCollapsed ? "hidden" : "visible", - width: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, + display: "inline-grid", + gridTemplateColumns: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, + gridTemplateRows: "max-content 1fr max-content", minWidth: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, - transitionProperty: "opacity", - transitionDuration: odysseyDesignTokens.TransitionDurationMain, + height: "100%", + transition: `grid-template-columns ${odysseyDesignTokens.TransitionDurationMain} opacity ${odysseyDesignTokens.TransitionDurationMain}`, transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, + overflow: "hidden", + opacity: isSideNavCollapsed ? 0 : 1, }), ); @@ -85,8 +80,9 @@ const StyledSideNav = styled("nav", { odysseyDesignTokens: DesignTokens; isSideNavCollapsed: boolean; }) => ({ - display: "flex", position: "relative", + display: "inline-block", + height: "100%", backgroundColor: odysseyDesignTokens.HueNeutralWhite, "&::after": { @@ -137,8 +133,7 @@ const SideNavHeaderContainer = styled("div", { hasContentScrolled: boolean; odysseyDesignTokens: DesignTokens; }) => ({ - position: "sticky", - top: 0, + flexShrink: 0, // The bottom border should appear only if the scrollable region has been scrolled ...(hasContentScrolled && ({ @@ -156,7 +151,7 @@ const SideNavListContainer = styled("ul")(() => ({ const SideNavScrollableContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ - flex: 1, + flex: "1 1 100%", overflowY: "auto", paddingInline: odysseyDesignTokens.Spacing2, })); @@ -189,8 +184,7 @@ const SideNavFooter = styled("div", { isContentScrollable: boolean; odysseyDesignTokens: DesignTokens; }) => ({ - position: "sticky", - bottom: 0, + flexShrink: 0, transitionProperty: "box-shadow", transitionDuration: odysseyDesignTokens.TransitionDurationMain, transitionTiming: odysseyDesignTokens.TransitionTimingMain, @@ -446,25 +440,21 @@ const SideNav = ({ ); return ( - - - {isCollapsible && ( - - )} - + + {isCollapsible && ( + + )} + @@ -478,7 +468,6 @@ const SideNav = ({ logoProps={logoProps} /> - - {!isLoading && (footerItems || hasCustomFooter) && ( )} - - + + ); }; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 4a2e0f6d3c..408e9369ad 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -14,7 +14,6 @@ import styled from "@emotion/styled"; import { memo } from "react"; import { Skeleton } from "@mui/material"; -import { Box } from "../../Box"; import { type DesignTokens, useOdysseyDesignTokens, @@ -24,6 +23,16 @@ import { SideNavProps } from "./types"; import { Heading6 } from "../../Typography"; import { TOP_NAV_HEIGHT } from "../TopNav"; +const SideNavHeaderContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", +})(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ + position: "relative", + display: "flex", + flexDirection: "column", + backgroundColor: odysseyDesignTokens.HueNeutralWhite, + zIndex: 1, +})); + const SideNavLogoContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ @@ -39,7 +48,7 @@ const SideNavLogoContainer = styled("div", { }, })); -const SideNavHeaderContainer = styled("div", { +const SideNavHeadingContainer = styled("div", { shouldForwardProp: (prop) => prop !== "odysseyDesignTokens", })(({ odysseyDesignTokens }: { odysseyDesignTokens: DesignTokens }) => ({ display: "flex", @@ -73,12 +82,7 @@ const SideNavHeader = ({ const odysseyDesignTokens = useOdysseyDesignTokens(); return ( - + {isLoading ? ( // The skeleton takes the hardcoded dimensions of the Okta logo @@ -88,10 +92,10 @@ const SideNavHeader = ({ )} - + {isLoading ? : appName} - - + + ); }; diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx index 851e343e68..350683d5be 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx @@ -131,7 +131,7 @@ const StyledToggleButton = styled(MuiButton, { height: odysseyDesignTokens.Spacing4, backgroundColor: odysseyDesignTokens.HueNeutral500, transform: "translate3d(-50%, -50%, 0)", - transition: `transform 250ms`, + transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`, }, }), ); diff --git a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties index b3763d23cd..d4f9e56125 100644 --- a/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties +++ b/packages/odyssey-react-mui/src/properties/odyssey-react-mui.properties @@ -85,8 +85,8 @@ severity.error = error severity.info = info severity.success = success severity.warning = warning -sidenav.toggle.expand = Expand side navigation -sidenav.toggle.collapse = Collapse side navigation +sidenav.toggle.expand = Open navigation +sidenav.toggle.collapse = Close navigation switch.active = Active switch.inactive = Inactive table.columnvisibility.arialabel = Show/hide columns From 542239bb77fc3aba095e8ca1a82b1fe347caf337 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 16:33:53 -0400 Subject: [PATCH 35/41] feat(odyssey-react-mui): updated grid transition timing --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index ef187a13fc..005bc9943e 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -62,7 +62,7 @@ const StyledCollapsibleContent = styled("div", { gridTemplateRows: "max-content 1fr max-content", minWidth: isSideNavCollapsed ? 0 : DEFAULT_SIDE_NAV_WIDTH, height: "100%", - transition: `grid-template-columns ${odysseyDesignTokens.TransitionDurationMain} opacity ${odysseyDesignTokens.TransitionDurationMain}`, + transition: `grid-template-columns ${odysseyDesignTokens.TransitionDurationMain}, opacity 300ms`, transitionTimingFunction: odysseyDesignTokens.TransitionTimingMain, overflow: "hidden", opacity: isSideNavCollapsed ? 0 : 1, From 5f97f2c8b1e57545898b8ac8929d4cadaa8e32ad Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Fri, 1 Nov 2024 17:00:10 -0400 Subject: [PATCH 36/41] feat(odyssey-react-mui): add id for button to control --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 005bc9943e..964d937a3f 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -442,6 +442,7 @@ const SideNav = ({ return ( From 844e3fc7f25a731c2b3395c01908e68b69a59605 Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Fri, 1 Nov 2024 16:11:54 -0500 Subject: [PATCH 37/41] fix: fixes type issues --- .../src/labs/SideNav/SideNavTest.test.tsx | 10 ++++------ .../src/labs/UiShell/UiShell.test.tsx | 5 ++--- .../src/labs/UiShell/renderUiShell.test.tsx | 1 - 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx index 9897e6fd35..3555e41220 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx @@ -38,13 +38,11 @@ describe("SideNav", () => { render( - } appName="Header text" + logoProps={{ + imageAltText: "Custom logo", + imageUrl: "https://placehold.co/600x400/EEE/31343C", + }} sideNavItems={[ { id: "item0", diff --git a/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx b/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx index 9e4f8f9615..6d2a912e80 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/UiShell.test.tsx @@ -26,9 +26,8 @@ describe("UiShell", () => { appRootElement={appRootElement} onSubscriptionCreated={() => {}} optionalComponents={{ - topNavLeftSide:
, sideNavFooter:
, - companyLogo:
, + topNavLeftSide:
, topNavRightSide: ( { test("renders optionally-available `componentSlots`", async () => { const optionalComponentTestIds: Array< keyof Required["optionalComponents"] - > = ["logoProps", "sideNavFooter"]; + > = ["sideNavFooter"]; // This is the subscription we give the component, and then once subscribed, we're going to immediately call it with new props. const subscribeToPropChanges: UiShellProps["subscribeToPropChanges"] = ( diff --git a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.test.tsx b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.test.tsx index 32df866869..bb8bcf134d 100644 --- a/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.test.tsx +++ b/packages/odyssey-react-mui/src/labs/UiShell/renderUiShell.test.tsx @@ -54,7 +54,6 @@ describe("renderUiShell", () => { }); expect(slottedElements.banners).toBeInstanceOf(HTMLDivElement); - expect(slottedElements.logoProps).toBeInstanceOf(HTMLDivElement); expect(slottedElements.sideNavFooter).toBeInstanceOf(HTMLDivElement); expect(slottedElements.topNavLeftSide).toBeInstanceOf(HTMLDivElement); expect(slottedElements.topNavRightSide).toBeInstanceOf(HTMLDivElement); From 778b3b3a0a2a1906308f92c7e81542ae7d33bd3f Mon Sep 17 00:00:00 2001 From: Kevin Ghadyani Date: Fri, 1 Nov 2024 16:25:42 -0500 Subject: [PATCH 38/41] fix: renames SideNavTest.test.ts to SideNav.test.ts --- .../SideNav/{SideNavTest.test.tsx => SideNav.test.tsx} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename packages/odyssey-react-mui/src/labs/SideNav/{SideNavTest.test.tsx => SideNav.test.tsx} (100%) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.test.tsx similarity index 100% rename from packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx rename to packages/odyssey-react-mui/src/labs/SideNav/SideNav.test.tsx index 3555e41220..1083474985 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavTest.test.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.test.tsx @@ -244,11 +244,11 @@ describe("SideNav", () => { }); test("displays sidenav link", async () => { - const menuLinkText = "Link"; - const menuClickableText = "Clickable"; - const headingText = "Heading"; - const accordionOuter = "Accordion outside"; const accordionInner = "Accordion inside"; + const accordionOuter = "Accordion outside"; + const headingText = "Heading"; + const menuClickableText = "Clickable"; + const menuLinkText = "Link"; render( From a011f0ab83acbf32f4de6e476ffd27c545ea9acb Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Mon, 4 Nov 2024 10:37:03 -0500 Subject: [PATCH 39/41] feat(odyssey-react-mui): updated button position. make count and severity mutually exclusive --- packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx | 2 +- .../src/labs/SideNav/SideNavItemLinkContent.tsx | 8 ++++++-- .../src/labs/SideNav/SideNavToggleButton.tsx | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 964d937a3f..3b40e88568 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -117,7 +117,7 @@ const StyledSideNav = styled("nav", { top: SIDENAV_COLLAPSE_ICON_POSITION, right: 0, transition: `transform ${odysseyDesignTokens.TransitionDurationMain}`, - transform: `translate3d(calc(100% + ${odysseyDesignTokens.Spacing1}), 0, 0)`, + transform: `translate3d(100%, 0, 0)`, }, }), ); diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemLinkContent.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemLinkContent.tsx index c81423386e..e8d319a986 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemLinkContent.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavItemLinkContent.tsx @@ -67,14 +67,18 @@ const SideNavItemLinkContent = ({ isIconVisible={Boolean(startIcon)} > {label} - {(severity || count) && ( + {!count && severity && ( - {count && } {severity && ( )} )} + {!severity && count && ( + + {count && } + + )} {endIcon && endIcon} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx index 350683d5be..ca3e6325f8 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx @@ -44,9 +44,10 @@ const StyledToggleButton = styled(MuiButton, { }) => ({ backgroundColor: "transparent", position: "relative", - padding: odysseyDesignTokens.Spacing3, - height: "unset", + width: odysseyDesignTokens.Spacing6, + height: odysseyDesignTokens.Spacing6, border: 0, + zIndex: 2, "&:hover, &:focus": { backgroundColor: "transparent", From 5640ebdcd54630b2914afa21412f42f7470656c2 Mon Sep 17 00:00:00 2001 From: bryancunningham-okta Date: Mon, 4 Nov 2024 11:23:38 -0500 Subject: [PATCH 40/41] feat(odyssey-react-mui): updated button focus ring. fix storybook tests --- .../src/labs/SideNav/SideNav.tsx | 1 + .../src/labs/SideNav/SideNavToggleButton.tsx | 5 ++++ .../odyssey-labs/SideNav/SideNav.stories.tsx | 27 ++++++++++--------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx index 3b40e88568..6345bde38a 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNav.tsx @@ -456,6 +456,7 @@ const SideNav = ({ )} diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx index ca3e6325f8..5d8c166f04 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavToggleButton.tsx @@ -49,6 +49,11 @@ const StyledToggleButton = styled(MuiButton, { border: 0, zIndex: 2, + "&:focus-visible": { + boxShadow: `inset 0 0 0 2px ${odysseyDesignTokens.PalettePrimaryMain}`, + outline: "none", + }, + "&:hover, &:focus": { backgroundColor: "transparent", 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 4dcad6493e..784bc13b6d 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 @@ -324,8 +324,14 @@ export const Default: StoryObj = { play: async ({ canvasElement, step }: PlaywrightProps) => { configure({ testIdAttribute: "data-se" }); const canvas = within(canvasElement); - const expandedRegion = canvas.getByTestId("expanded-region"); - const collapsedRegion = canvas.getByTestId("collapsed-region"); + + const toggleButton = canvas.getByRole("button", { + name: "Close navigation", + }); + const navElement = canvas.getByRole("navigation", { + name: "Main navigation", + }); + // const collapsedRegion = canvas.getByTestId("collapsed-region"); const scrollableRegion = canvas.getByTestId("scrollable-region"); /** @@ -352,21 +358,18 @@ export const Default: StoryObj = { } }); await step("Side Nav Collapse", async ({}) => { - const collapseButton = within(collapsedRegion).getByRole("button", { - name: "collapse side navigation", - }); - await userEvent.click(collapseButton); + await userEvent.click(toggleButton); + await waitFor(() => { - expect(expandedRegion).not.toBeVisible(); + expect(toggleButton.ariaExpanded).toEqual("false"); + expect(navElement).toHaveStyle({ width: 0 }); }); }); await step("Side Nav Expand", async ({}) => { - const expandeButton = within(collapsedRegion).getByRole("button", { - name: "expand side navigation", - }); - await userEvent.click(expandeButton); + await userEvent.click(toggleButton); await waitFor(() => { - expect(expandedRegion).toBeVisible(); + expect(toggleButton.ariaExpanded).toEqual("true"); + expect(navElement).toBeVisible(); }); }); }, From 53023e9a22b264325b0a710bdcd11190963051e1 Mon Sep 17 00:00:00 2001 From: Jordan Koschei Date: Mon, 4 Nov 2024 12:08:56 -0500 Subject: [PATCH 41/41] refactor: make specific CSS less specific --- packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx index 408e9369ad..7232e6d614 100644 --- a/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx +++ b/packages/odyssey-react-mui/src/labs/SideNav/SideNavHeader.tsx @@ -57,7 +57,7 @@ const SideNavHeadingContainer = styled("div", { padding: odysseyDesignTokens.Spacing4, width: "100%", - h2: { + ["& .MuiTypography-root"]: { margin: 0, width: "100%", },