diff --git a/.changeset/lovely-snakes-approve.md b/.changeset/lovely-snakes-approve.md new file mode 100644 index 0000000000..d780585257 --- /dev/null +++ b/.changeset/lovely-snakes-approve.md @@ -0,0 +1,7 @@ +--- +"@nextui-org/popover": patch +"@nextui-org/system-rsc": patch +"@nextui-org/theme": patch +--- + +Fixed unexpected props on a DOM element (#2474) diff --git a/packages/components/dropdown/__tests__/dropdown.test.tsx b/packages/components/dropdown/__tests__/dropdown.test.tsx index 37b0eb82e6..71aab8f9ab 100644 --- a/packages/components/dropdown/__tests__/dropdown.test.tsx +++ b/packages/components/dropdown/__tests__/dropdown.test.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import {act, render} from "@testing-library/react"; import {Button} from "@nextui-org/button"; import userEvent from "@testing-library/user-event"; +import {User} from "@nextui-org/user"; import {Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, DropdownSection} from "../src"; @@ -416,4 +417,48 @@ describe("Dropdown", () => { expect(onSelectionChange).toBeCalledTimes(0); }); + + it("should render without error (custom trigger + isDisabled)", async () => { + const spy = jest.spyOn(console, "error").mockImplementation(() => {}); + + render( + + +
Trigger
+
+ + New file + Copy link + Edit file + + Delete file + + +
, + ); + + expect(spy).toBeCalledTimes(0); + + spy.mockRestore(); + + render( + + + + + + New file + Copy link + Edit file + + Delete file + + + , + ); + + expect(spy).toBeCalledTimes(0); + + spy.mockRestore(); + }); }); diff --git a/packages/components/popover/src/popover-trigger.tsx b/packages/components/popover/src/popover-trigger.tsx index dded9c1e70..a32a584c02 100644 --- a/packages/components/popover/src/popover-trigger.tsx +++ b/packages/components/popover/src/popover-trigger.tsx @@ -1,6 +1,6 @@ import React, {Children, cloneElement, useMemo} from "react"; -import {forwardRef} from "@nextui-org/system"; -import {pickChildren} from "@nextui-org/react-utils"; +import {forwardRef, isNextUIEl} from "@nextui-org/system"; +import {pickChildren, filterDOMProps} from "@nextui-org/react-utils"; import {useAriaButton} from "@nextui-org/use-aria-button"; import {Button} from "@nextui-org/button"; import {mergeProps} from "@react-aria/utils"; @@ -29,7 +29,7 @@ const PopoverTrigger = forwardRef<"button", PopoverTriggerProps>((props, _) => { }; }, [children]); - const {onPress, ...rest} = useMemo(() => { + const {onPress, ...restProps} = useMemo(() => { return getTriggerProps(mergeProps(otherProps, child.props), child.ref); }, [getTriggerProps, child.props, otherProps, child.ref]); @@ -42,7 +42,22 @@ const PopoverTrigger = forwardRef<"button", PopoverTriggerProps>((props, _) => { return triggerChildren?.[0] !== undefined; }, [triggerChildren]); - return cloneElement(child, mergeProps(rest, hasNextUIButton ? {onPress} : buttonProps)); + const isDisabled = !!restProps?.isDisabled; + + const isNextUIElement = isNextUIEl(child); + + return cloneElement( + child, + mergeProps( + // if we add `isDisabled` prop to DOM elements, + // react will fail to recognize it on a DOM element, + // hence, apply filterDOMProps for such case + filterDOMProps(restProps, { + enabled: isDisabled && !isNextUIElement, + }), + hasNextUIButton ? {onPress} : buttonProps, + ), + ); }); PopoverTrigger.displayName = "NextUI.PopoverTrigger"; diff --git a/packages/components/popover/src/use-popover.ts b/packages/components/popover/src/use-popover.ts index d7593b0371..df0e72dd34 100644 --- a/packages/components/popover/src/use-popover.ts +++ b/packages/components/popover/src/use-popover.ts @@ -244,7 +244,12 @@ export function usePopover(originalProps: UsePopoverProps) { "aria-haspopup": "dialog", ...mergeProps(triggerProps, props), onPress, - className: slots.trigger({class: clsx(classNames?.trigger, props.className)}), + className: slots.trigger({ + class: clsx(classNames?.trigger, props.className), + // apply isDisabled class names to make the trigger child disabled + // e.g. for elements like div or NextUI elements that don't have `isDisabled` prop + isDropdownDisabled: !!props?.isDisabled, + }), ref: mergeRefs(_ref, triggerRef), }; }, diff --git a/packages/core/system-rsc/src/utils.ts b/packages/core/system-rsc/src/utils.ts index 884e8341e5..151c1536ab 100644 --- a/packages/core/system-rsc/src/utils.ts +++ b/packages/core/system-rsc/src/utils.ts @@ -100,3 +100,12 @@ export const mapPropsVariantsWithCommon = < * Classnames utility */ export const cn = clsx; + +/** + * Checks if a component is a NextUI component. + * @param component - The component to check. + * @returns `true` if the component is a NextUI component, `false` otherwise. + */ +export const isNextUIEl = (component: React.ReactComponentElement) => { + return !!component.type?.render?.displayName?.includes("NextUI"); +}; diff --git a/packages/core/theme/src/components/popover.ts b/packages/core/theme/src/components/popover.ts index a10af68039..e845146c90 100644 --- a/packages/core/theme/src/components/popover.ts +++ b/packages/core/theme/src/components/popover.ts @@ -162,6 +162,12 @@ const popover = tv({ base: "animate-none", }, }, + isDropdownDisabled: { + true: { + trigger: "opacity-disabled pointer-events-none", + }, + false: {}, + }, }, defaultVariants: { color: "default",