diff --git a/.changeset/tender-avocados-develop.md b/.changeset/tender-avocados-develop.md new file mode 100644 index 00000000000..c5357afbc83 --- /dev/null +++ b/.changeset/tender-avocados-develop.md @@ -0,0 +1,8 @@ +--- +"@salt-ds/core": minor +--- + +add `render` prop to `NavigationItem` + +- the `render` prop enables the substitution of the default anchor tag with an alternate link, such as React Router, facilitating integration with routing libraries. +- parent items can now be modified to act as hyperlinks to specific pages while still retaining the ability to expand and collapse rows diff --git a/packages/core/src/__tests__/__e2e__/navigation-item/NavigationItem.cy.tsx b/packages/core/src/__tests__/__e2e__/navigation-item/NavigationItem.cy.tsx index e67007d18c4..175c5193095 100644 --- a/packages/core/src/__tests__/__e2e__/navigation-item/NavigationItem.cy.tsx +++ b/packages/core/src/__tests__/__e2e__/navigation-item/NavigationItem.cy.tsx @@ -1,4 +1,4 @@ -import { NavigationItem } from "@salt-ds/core"; +import { NavigationItem, NavigationItemRenderProps } from "@salt-ds/core"; import { NotificationIcon } from "@salt-ds/icons"; describe("GIVEN a NavItem", () => { @@ -99,5 +99,68 @@ describe("GIVEN a NavItem", () => { ); }); }); + + describe("AND `render` is passed", () => { + it("should call `render` to create parent item", () => { + const mockRender = cy.stub().as("render"); + cy.mount( + + Navigation Item + + ); + cy.get("@render").should("have.been.calledWithMatch", { + isParent: true, + active: true, + elementProps: { + "aria-expanded": true, + "aria-label": "expand", + className: Cypress.sinon.match.string, + children: Cypress.sinon.match.any, + }, + expanded: true, + href: "https://www.saltdesignsystem.com", + level: 2, + orientation: "vertical", + }); + }); + it("should call `render` to create child item", () => { + const mockRender = cy.stub().as("render"); + cy.mount( + + Navigation Item + + ); + cy.get("@render").should("have.been.calledWithMatch", { + isParent: false, + active: true, + elementProps: { + "aria-current": "page", + "aria-label": "change page", + className: Cypress.sinon.match.string, + children: Cypress.sinon.match.any, + }, + expanded: true, + href: "https://www.saltdesignsystem.com", + level: 2, + orientation: "vertical", + }); + }); + }); }); }); diff --git a/packages/core/src/navigation-item/ConditionalWrapper.tsx b/packages/core/src/navigation-item/ConditionalWrapper.tsx deleted file mode 100644 index 8aa4f7c9d86..00000000000 --- a/packages/core/src/navigation-item/ConditionalWrapper.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { MouseEvent, ReactNode } from "react"; -import { NavigationItemProps } from "./NavigationItem"; - -interface ConditionalWrapperProps - extends Pick< - NavigationItemProps, - "parent" | "expanded" | "onExpand" | "active" | "href" | "onClick" - > { - children: ReactNode; - className: string; -} - -export const ConditionalWrapper = ({ - children, - className, - parent, - expanded, - onExpand, - active, - href, -}: ConditionalWrapperProps) => { - const handleExpand = (event: MouseEvent) => { - event.stopPropagation(); - onExpand?.(event); - }; - - return parent || href === undefined ? ( - - ) : ( - - {children} - - ); -}; diff --git a/packages/core/src/navigation-item/NavigationItem.tsx b/packages/core/src/navigation-item/NavigationItem.tsx index db8e0112e68..f0977ef58de 100644 --- a/packages/core/src/navigation-item/NavigationItem.tsx +++ b/packages/core/src/navigation-item/NavigationItem.tsx @@ -1,14 +1,60 @@ +import React, { + AriaAttributes, + ForwardedRef, + MouseEvent, + ReactElement, +} from "react"; import { ComponentPropsWithoutRef, forwardRef, MouseEventHandler } from "react"; import { makePrefixer } from "../utils"; import { clsx } from "clsx"; import { ExpansionIcon } from "./ExpansionIcon"; -import { ConditionalWrapper } from "./ConditionalWrapper"; import { useWindow } from "@salt-ds/window"; import { useComponentCssInjection } from "@salt-ds/styles"; import navigationItemCss from "./NavigationItem.css"; +type OptionalPartial = Partial>; + +export interface NavigationItemElementProps< + T extends HTMLAnchorElement | HTMLButtonElement +> extends Pick< + AriaAttributes, + "aria-label" | "aria-expanded" | "aria-current" + > { + /** + * Item's children. + */ + children: ReactElement; + /** + * Item's className. + */ + className: string; + /** + * Handler to expand groups. + */ + onClick?: MouseEventHandler; +} + +export interface NavigationItemRenderProps< + T extends HTMLAnchorElement | HTMLButtonElement +> extends OptionalPartial< + NavigationItemProps, + "active" | "expanded" | "level" | "orientation" + > { + /** + * If the item to render is expandable when clicked. + */ + isParent: boolean; + /** + * Props to apply to the row's element. + */ + elementProps: NavigationItemElementProps; + /** + * If row should load page when clicked, the corresponding href. + */ + href?: string; +} export interface NavigationItemProps extends ComponentPropsWithoutRef<"div"> { /** * Whether the navigation item is active. @@ -34,76 +80,126 @@ export interface NavigationItemProps extends ComponentPropsWithoutRef<"div"> { * Whether the navigation item is a parent with nested items. */ parent?: boolean; + /** + * Render prop to enable customisation of navigation item element. + */ + render?: React.FC< + NavigationItemRenderProps + >; /** * Action to be triggered when the navigation item is expanded. */ - onExpand?: MouseEventHandler; + onExpand?: MouseEventHandler; /** - * Href to be passed to the Link element. + * Page to load, when this navigation item is selected. */ href?: string; } const withBaseName = makePrefixer("saltNavigationItem"); -export const NavigationItem = forwardRef( - function NavigationItem(props, ref) { - const { - active, - blurActive, - children, - className, - expanded = false, - orientation = "horizontal", - parent, - level = 0, - onExpand, - href, - style: styleProp, - ...rest - } = props; +export const NavigationItem = forwardRef(function NavigationItem( + props: NavigationItemProps, + ref: ForwardedRef +) { + const defaultRender: React.FC< + NavigationItemRenderProps + > = (props) => { + const { isParent, elementProps, href } = props; + /** Parent rows just expand and collapse and are not links */ + if (isParent || !href) { + return