diff --git a/.changeset/nasty-bees-sniff.md b/.changeset/nasty-bees-sniff.md new file mode 100644 index 0000000000..3f129edff2 --- /dev/null +++ b/.changeset/nasty-bees-sniff.md @@ -0,0 +1,7 @@ +--- +"@nextui-org/listbox": patch +"@nextui-org/menu": patch +"@nextui-org/pagination": patch +--- + +Fixes missing `
  • ` wrapper when `href` prop is passed in `ListboxItem`, `MenuItem`, and `PaginationItem` (#4147) \ No newline at end of file diff --git a/packages/components/listbox/__tests__/listbox.test.tsx b/packages/components/listbox/__tests__/listbox.test.tsx index 60614536bf..e77196aed1 100644 --- a/packages/components/listbox/__tests__/listbox.test.tsx +++ b/packages/components/listbox/__tests__/listbox.test.tsx @@ -124,6 +124,40 @@ describe("Listbox", () => { expect(() => wrapper.unmount()).not.toThrow(); }); + it("should not have anchor tag when href prop is not passed", () => { + render( + + New file + Copy link + Edit file + , + ); + + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeFalsy(); + }); + + it("should have anchor tag when href prop is passed", () => { + const href = "http://www.nextui.org/"; + + render( + + + New file + + Copy link + Edit file + , + ); + + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeTruthy(); + + expect(anchorTag).toHaveProperty("href", href); + }); + it("should work with single selection (controlled)", async () => { let onSelectionChange = jest.fn(); diff --git a/packages/components/listbox/src/listbox-item.tsx b/packages/components/listbox/src/listbox-item.tsx index f2e8010f74..bf624a9e3b 100644 --- a/packages/components/listbox/src/listbox-item.tsx +++ b/packages/components/listbox/src/listbox-item.tsx @@ -12,6 +12,7 @@ export interface ListboxItemProps extends UseListboxI const ListboxItem = forwardRef<"li", ListboxItemProps>((props, _) => { const { Component, + FragmentWrapper, rendered, description, isSelectable, @@ -22,6 +23,7 @@ const ListboxItem = forwardRef<"li", ListboxItemProps>((props, _) => { endContent, hideSelectedIcon, disableAnimation, + fragmentWrapperProps, getItemProps, getLabelProps, getWrapperProps, @@ -45,19 +47,21 @@ const ListboxItem = forwardRef<"li", ListboxItemProps>((props, _) => { return ( - {startContent} - {description ? ( -
    + + {startContent} + {description ? ( +
    + {rendered} + {description} +
    + ) : ( {rendered} - {description} -
    - ) : ( - {rendered} - )} - {isSelectable && !hideSelectedIcon && ( - {selectedContent} - )} - {endContent} + )} + {isSelectable && !hideSelectedIcon && ( + {selectedContent} + )} + {endContent} +
    ); }); diff --git a/packages/components/listbox/src/use-listbox-item.ts b/packages/components/listbox/src/use-listbox-item.ts index 37103f2107..43be909f3c 100644 --- a/packages/components/listbox/src/use-listbox-item.ts +++ b/packages/components/listbox/src/use-listbox-item.ts @@ -1,6 +1,6 @@ import type {ListboxItemBaseProps} from "./base/listbox-item-base"; -import {useMemo, useRef, useCallback} from "react"; +import {useMemo, useRef, useCallback, Fragment} from "react"; import {listboxItem} from "@nextui-org/theme"; import { HTMLNextUIProps, @@ -48,6 +48,7 @@ export function useListboxItem(originalProps: UseListboxItemPr shouldHighlightOnFocus, hideSelectedIcon = false, isReadOnly = false, + href, ...otherProps } = props; @@ -56,9 +57,12 @@ export function useListboxItem(originalProps: UseListboxItemPr const domRef = useRef(null); - const Component = as || (originalProps.href ? "a" : "li"); + const Component = as || "li"; const shouldFilterDOMProps = typeof Component === "string"; + const FragmentWrapper = href ? "a" : Fragment; + const fragmentWrapperProps = href ? {href} : {}; + const {rendered, key} = item; const isDisabled = state.disabledKeys.has(key) || originalProps.isDisabled; @@ -167,6 +171,7 @@ export function useListboxItem(originalProps: UseListboxItemPr return { Component, + FragmentWrapper, domRef, slots, classNames, @@ -180,6 +185,7 @@ export function useListboxItem(originalProps: UseListboxItemPr selectedIcon, hideSelectedIcon, disableAnimation, + fragmentWrapperProps, getItemProps, getLabelProps, getWrapperProps, diff --git a/packages/components/menu/__tests__/menu.test.tsx b/packages/components/menu/__tests__/menu.test.tsx index eeaa3eb068..0b4ea75e87 100644 --- a/packages/components/menu/__tests__/menu.test.tsx +++ b/packages/components/menu/__tests__/menu.test.tsx @@ -125,6 +125,46 @@ describe("Menu", () => { expect(() => wrapper.unmount()).not.toThrow(); }); + it("should not have anchor tag when href prop is not passed", () => { + render( + + New file + Copy link + Edit file + + Delete file + + , + ); + + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeFalsy(); + }); + + it("should have anchor tag when href prop is passed", () => { + const href = "http://www.nextui.org/"; + + render( + + + New file + + Copy link + Edit file + + Delete file + + , + ); + + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeTruthy(); + + expect(anchorTag).toHaveProperty("href", href); + }); + it("should work with single selection (controlled)", async () => { let onSelectionChange = jest.fn(); diff --git a/packages/components/menu/src/menu-item.tsx b/packages/components/menu/src/menu-item.tsx index 226d28fd85..ac05b98aa9 100644 --- a/packages/components/menu/src/menu-item.tsx +++ b/packages/components/menu/src/menu-item.tsx @@ -1,5 +1,6 @@ import {useMemo, ReactNode} from "react"; import {forwardRef} from "@nextui-org/system"; +import * as React from "react"; import {UseMenuItemProps, useMenuItem} from "./use-menu-item"; import {MenuSelectedIcon} from "./menu-selected-icon"; @@ -12,6 +13,7 @@ export interface MenuItemProps extends UseMenuItemPro const MenuItem = forwardRef<"li", MenuItemProps>((props, _) => { const { Component, + FragmentWrapper, slots, classNames, rendered, @@ -25,6 +27,7 @@ const MenuItem = forwardRef<"li", MenuItemProps>((props, _) => { endContent, disableAnimation, hideSelectedIcon, + fragmentWrapperProps, getItemProps, getLabelProps, getDescriptionProps, @@ -48,20 +51,22 @@ const MenuItem = forwardRef<"li", MenuItemProps>((props, _) => { return ( - {startContent} - {description ? ( -
    + + {startContent} + {description ? ( +
    + {rendered} + {description} +
    + ) : ( {rendered} - {description} -
    - ) : ( - {rendered} - )} - {shortcut && {shortcut}} - {isSelectable && !hideSelectedIcon && ( - {selectedContent} - )} - {endContent} + )} + {shortcut && {shortcut}} + {isSelectable && !hideSelectedIcon && ( + {selectedContent} + )} + {endContent} +
    ); }); diff --git a/packages/components/menu/src/use-menu-item.ts b/packages/components/menu/src/use-menu-item.ts index 4d40050d00..9ed4e31253 100644 --- a/packages/components/menu/src/use-menu-item.ts +++ b/packages/components/menu/src/use-menu-item.ts @@ -1,7 +1,7 @@ import type {MenuItemBaseProps} from "./base/menu-item-base"; import type {Node} from "@react-types/shared"; -import {useMemo, useRef, useCallback} from "react"; +import {useMemo, useRef, useCallback, Fragment} from "react"; import {menuItem} from "@nextui-org/theme"; import { HTMLNextUIProps, @@ -57,6 +57,7 @@ export function useMenuItem(originalProps: UseMenuItemProps isReadOnly = false, closeOnSelect, onClose, + href, ...otherProps } = props; @@ -65,9 +66,12 @@ export function useMenuItem(originalProps: UseMenuItemProps const domRef = useRef(null); - const Component = as || (otherProps?.href ? "a" : "li"); + const Component = as || "li"; const shouldFilterDOMProps = typeof Component === "string"; + const FragmentWrapper = href ? "a" : Fragment; + const fragmentWrapperProps = href ? {href} : {}; + const {rendered, key} = item; const isDisabledProp = state.disabledKeys.has(key) || originalProps.isDisabled; @@ -192,6 +196,7 @@ export function useMenuItem(originalProps: UseMenuItemProps return { Component, + FragmentWrapper, domRef, slots, classNames, @@ -205,6 +210,7 @@ export function useMenuItem(originalProps: UseMenuItemProps endContent, selectedIcon, disableAnimation, + fragmentWrapperProps, getItemProps, getLabelProps, hideSelectedIcon, diff --git a/packages/components/pagination/__tests__/pagination.test.tsx b/packages/components/pagination/__tests__/pagination.test.tsx index 0a0ff16376..71bffe0e43 100644 --- a/packages/components/pagination/__tests__/pagination.test.tsx +++ b/packages/components/pagination/__tests__/pagination.test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import {render} from "@testing-library/react"; -import {Pagination} from "../src"; +import {Pagination, PaginationItem} from "../src"; describe("Pagination", () => { it("should render correctly", () => { @@ -37,6 +37,25 @@ describe("Pagination", () => { expect(prevButton).toBeNull(); }); + it("should not have anchor tag when href prop is not passed", () => { + render(); + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeFalsy(); + }); + + it("should have anchor tag when href prop is passed", () => { + const href = "http://www.nextui.org/"; + + render(); + + let anchorTag = document.getElementsByTagName("a")[0]; + + expect(anchorTag).toBeTruthy(); + + expect(anchorTag).toHaveProperty("href", href); + }); + it("should show dots when total is greater than 10", () => { const wrapper = render(); diff --git a/packages/components/pagination/src/pagination-item.tsx b/packages/components/pagination/src/pagination-item.tsx index 988e554b23..03ef05c91c 100644 --- a/packages/components/pagination/src/pagination-item.tsx +++ b/packages/components/pagination/src/pagination-item.tsx @@ -5,9 +5,14 @@ import {usePaginationItem, UsePaginationItemProps} from "./use-pagination-item"; export interface PaginationItemProps extends UsePaginationItemProps {} const PaginationItem = forwardRef<"li", PaginationItemProps>((props, ref) => { - const {Component, children, getItemProps} = usePaginationItem({...props, ref}); + const {Component, FragmentWrapper, fragmentWrapperProps, children, getItemProps} = + usePaginationItem({...props, ref}); - return {children}; + return ( + + {children} + + ); }); PaginationItem.displayName = "NextUI.PaginationItem"; diff --git a/packages/components/pagination/src/use-pagination-item.ts b/packages/components/pagination/src/use-pagination-item.ts index 1bef299b98..e507d3aaaf 100644 --- a/packages/components/pagination/src/use-pagination-item.ts +++ b/packages/components/pagination/src/use-pagination-item.ts @@ -2,7 +2,7 @@ import type {Ref} from "react"; import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system"; import type {LinkDOMProps, PressEvent} from "@react-types/shared"; -import {useMemo} from "react"; +import {Fragment, useMemo} from "react"; import {PaginationItemValue} from "@nextui-org/use-pagination"; import {clsx, dataAttr} from "@nextui-org/shared-utils"; import {chain, mergeProps, shouldClientNavigate, useRouter} from "@react-aria/utils"; @@ -64,10 +64,13 @@ export function usePaginationItem(props: UsePaginationItemProps) { } = props; const isLink = !!props?.href; - const Component = as || isLink ? "a" : "li"; + const Component = as || "li"; const shouldFilterDOMProps = typeof Component === "string"; - const domRef = useDOMRef(ref); + const FragmentWrapper = isLink ? "a" : Fragment; + const fragmentWrapperProps = isLink ? {href: props.href} : {}; + + const domRef = useDOMRef(ref); const router = useRouter(); const ariaLabel = useMemo( @@ -129,6 +132,8 @@ export function usePaginationItem(props: UsePaginationItemProps) { return { Component, + FragmentWrapper, + fragmentWrapperProps, children, ariaLabel, isFocused,