fix(popover): unexpected props on a DOM element (#2522)

* fix(popover): handle isDisabled logic for elements without isDisabled props

* chore(popover): isDisabled not necessary in restProps

* chore(changset): handle isDisabled logic for elements without isDisabled props

* fix(popover): keep all the props but isDisabled for non nextui button

* refactor(popover): move isDisabled handling to getTriggerProps

* refactor(popover): get the popover trigger styles from theme instead

* feat(theme): add isDisabled styles in popover

* chore(changeset): add patch to @nextui-org/theme

* refactor(popover): avoid re-instantiate popover styles

* fix(popover): apply filterDOMProps in popover trigger

* fix(popover): avoid conflicts with tooltip isDisabled

* chore(core): add isNextUIEl function to check if a component is a NextUI component

* chore(changeset): add system-rsc and revise message

* feat(dropdown): add tests for custom trigger with isDisabled

* fix(dropdown): incorrect User import path

* feat(dropdown): revise User and add mockRestore

* fix(dropdown): revise user import path

---------

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
աӄա 2024-04-02 09:33:26 +08:00 committed by GitHub
parent 410e30c720
commit c5049edfde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 5 deletions

View File

@ -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)

View File

@ -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(
<Dropdown isDisabled>
<DropdownTrigger>
<div>Trigger</div>
</DropdownTrigger>
<DropdownMenu aria-label="Actions" onAction={alert}>
<DropdownItem key="new">New file</DropdownItem>
<DropdownItem key="copy">Copy link</DropdownItem>
<DropdownItem key="edit">Edit file</DropdownItem>
<DropdownItem key="delete" color="danger">
Delete file
</DropdownItem>
</DropdownMenu>
</Dropdown>,
);
expect(spy).toBeCalledTimes(0);
spy.mockRestore();
render(
<Dropdown isDisabled>
<DropdownTrigger>
<User as="button" description="@tonyreichert" name="Tony Reichert" />
</DropdownTrigger>
<DropdownMenu aria-label="Actions" onAction={alert}>
<DropdownItem key="new">New file</DropdownItem>
<DropdownItem key="copy">Copy link</DropdownItem>
<DropdownItem key="edit">Edit file</DropdownItem>
<DropdownItem key="delete" color="danger">
Delete file
</DropdownItem>
</DropdownMenu>
</Dropdown>,
);
expect(spy).toBeCalledTimes(0);
spy.mockRestore();
});
});

View File

@ -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";

View File

@ -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),
};
},

View File

@ -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<any>) => {
return !!component.type?.render?.displayName?.includes("NextUI");
};

View File

@ -162,6 +162,12 @@ const popover = tv({
base: "animate-none",
},
},
isDropdownDisabled: {
true: {
trigger: "opacity-disabled pointer-events-none",
},
false: {},
},
},
defaultVariants: {
color: "default",