Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/two-flies-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@nextui-org/use-aria-modal-overlay": patch
"@nextui-org/use-aria-button": patch
"@nextui-org/aria-utils": patch
"@nextui-org/dropdown": patch
"@nextui-org/use-aria-link": patch
"@nextui-org/listbox": patch
"@nextui-org/button": patch
"@nextui-org/navbar": patch
"@nextui-org/card": patch
"@nextui-org/link": patch
"@nextui-org/menu": patch
---

Fix #4292 interactive elements were not responding to "onClick" event
12 changes: 12 additions & 0 deletions packages/components/button/__tests__/button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@testing-library/jest-dom";
import * as React from "react";
import {render} from "@testing-library/react";
import userEvent, {UserEvent} from "@testing-library/user-event";
Expand Down Expand Up @@ -35,6 +36,17 @@ describe("Button", () => {
expect(onPress).toHaveBeenCalled();
});

it("should trigger onClick function", async () => {
const onClick = jest.fn();
const {getByRole} = render(<Button disableRipple onClick={onClick} />);

const button = getByRole("button");

await user.click(button);

expect(onClick).toHaveBeenCalled();
});

it("should ignore events when disabled", async () => {
const onPress = jest.fn();
const {getByRole} = render(<Button disableRipple isDisabled onPress={onPress} />);
Expand Down
3 changes: 2 additions & 1 deletion packages/components/button/src/use-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ interface Props extends HTMLNextUIProps<"button"> {
/**
* The native button click event handler.
* use `onPress` instead.
* @deprecated
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
}
Expand Down Expand Up @@ -150,7 +151,7 @@ export function useButton(props: UseButtonProps) {
elementType: as,
isDisabled,
onPress: chain(onPress, handlePress),
onClick: onClick,
onClick,
...otherProps,
} as AriaButtonProps,
domRef,
Expand Down
11 changes: 8 additions & 3 deletions packages/components/button/stories/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,19 @@ const defaultProps = {
const StateTemplate = (args: ButtonProps) => {
const [isOpen, setIsOpen] = React.useState(false);

const handlePress = () => {
const handlePress = (e: any) => {
// eslint-disable-next-line no-console
console.log("Pressed");
console.log("Pressed", e);
Comment on lines +74 to +76
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace any type with specific event type

Using any type reduces type safety. Since this is a button click event, we should use the specific event type.

-const handlePress = (e: any) => {
+const handlePress = (e: React.MouseEvent<HTMLButtonElement>) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePress = (e: any) => {
// eslint-disable-next-line no-console
console.log("Pressed");
console.log("Pressed", e);
const handlePress = (e: React.MouseEvent<HTMLButtonElement>) => {
// eslint-disable-next-line no-console
console.log("Pressed", e);

setIsOpen((prev) => !prev);
};

return (
<Button {...args} aria-label="Open" aria-pressed={isOpen} onPress={handlePress}>
<Button
{...args}
aria-label={isOpen ? "Close" : "Open"}
aria-pressed={isOpen}
onClick={handlePress}
>
Comment on lines +81 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update to use onPress instead of onClick

Since onClick is being deprecated in favor of onPress, the story should reflect this change to demonstrate the recommended usage pattern.

 <Button
   {...args}
   aria-label={isOpen ? "Close" : "Open"}
   aria-pressed={isOpen}
-  onClick={handlePress}
+  onPress={handlePress}
 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button
{...args}
aria-label={isOpen ? "Close" : "Open"}
aria-pressed={isOpen}
onClick={handlePress}
>
<Button
{...args}
aria-label={isOpen ? "Close" : "Open"}
aria-pressed={isOpen}
onPress={handlePress}
>

{isOpen ? "Close" : "Open"}
</Button>
);
Expand Down
21 changes: 18 additions & 3 deletions packages/components/card/__tests__/card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import * as React from "react";
import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import userEvent, {UserEvent} from "@testing-library/user-event";

import {Card} from "../src";

describe("Card", () => {
let user: UserEvent;

beforeEach(() => {
user = userEvent.setup();
});

it("should render correctly", () => {
const wrapper = render(<Card />);

Expand All @@ -30,13 +36,22 @@ describe("Card", () => {

const button = getByRole("button");

const user = userEvent.setup();

await user.click(button);

expect(onPress).toHaveBeenCalled();
});

it("should trigger onClick function", async () => {
const onClick = jest.fn();
const {getByRole} = render(<Card disableRipple isPressable onClick={onClick} />);

const button = getByRole("button");

await user.click(button);

expect(onClick).toHaveBeenCalled();
});

it("should render correctly when nested", () => {
const wrapper = render(
<Card>
Expand Down
11 changes: 8 additions & 3 deletions packages/components/card/src/use-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {AriaButtonProps} from "@nextui-org/use-aria-button";
import type {RippleProps} from "@nextui-org/ripple";

import {card} from "@nextui-org/theme";
import {ReactNode, useCallback, useMemo} from "react";
import {MouseEventHandler, ReactNode, useCallback, useMemo} from "react";
import {chain, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {PressEvent, useHover} from "@react-aria/interactions";
Expand All @@ -20,7 +20,7 @@ import {ReactRef, filterDOMProps} from "@nextui-org/react-utils";
import {useDOMRef} from "@nextui-org/react-utils";
import {useRipple} from "@nextui-org/ripple";

export interface Props extends HTMLNextUIProps<"div"> {
export interface Props extends Omit<HTMLNextUIProps<"div">, "onClick"> {
/**
* Ref to the DOM node.
*/
Expand All @@ -34,12 +34,17 @@ export interface Props extends HTMLNextUIProps<"div"> {
* @default false
*/
disableRipple?: boolean;

/**
* Whether the card should allow text selection on press. (only for pressable cards)
* @default true
*/
allowTextSelectionOnPress?: boolean;
/**
* The native button click event handler.
* use `onPress` instead.
* @deprecated
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
/**
* Classname or List of classes to change the classNames of the element.
* if `className` is passed, it will be added to the base slot.
Expand Down
30 changes: 30 additions & 0 deletions packages/components/card/stories/card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,28 @@ const PrimaryActionTemplate = (args: CardProps) => {
);
};

const PressableTemplate = (args: CardProps) => {
// Both events should be fired when clicking on the card

const handlePress = () => {
// eslint-disable-next-line no-console
alert("card pressed");
};

const onClick = () => {
// eslint-disable-next-line no-console
alert("card clicked");
};

return (
<Card {...args} isPressable onClick={onClick} onPress={handlePress}>
<CardBody>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</CardBody>
</Card>
);
};

const CenterImgWithHeaderTemplate = (args: CardProps) => {
const list = [
{
Expand Down Expand Up @@ -414,6 +436,14 @@ export const Default = {
},
};

export const Pressable = {
render: PressableTemplate,

args: {
...defaultProps,
},
};

export const WithDivider = {
render: WithDividerTemplate,

Expand Down
4 changes: 3 additions & 1 deletion packages/components/dropdown/stories/dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ const Template = ({
<Button>{label}</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Actions" color={color} variant={variant}>
<DropdownItem key="new">New file</DropdownItem>
<DropdownItem key="new" onClick={() => alert("New file")}>
New file
</DropdownItem>
Comment on lines +151 to +153
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider removing the onClick handler in favor of onAction

The component already has an onAction handler at the Dropdown level that triggers an alert. Adding an onClick handler here will cause:

  1. Duplicate alerts since both handlers will fire
  2. Inconsistency with other items that only use onAction

Remove the onClick handler since the action is already handled by onAction:

-      <DropdownItem key="new" onClick={() => alert("New file")}>
+      <DropdownItem key="new">
        New file
      </DropdownItem>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<DropdownItem key="new" onClick={() => alert("New file")}>
New file
</DropdownItem>
<DropdownItem key="new">
New file
</DropdownItem>

<DropdownItem key="copy">Copy link</DropdownItem>
<DropdownItem key="edit">Edit file</DropdownItem>
<DropdownItem key="delete" className="text-danger" color="danger">
Expand Down
29 changes: 29 additions & 0 deletions packages/components/link/__tests__/link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import * as React from "react";
import {render} from "@testing-library/react";
import userEvent, {UserEvent} from "@testing-library/user-event";

import {Link} from "../src";

describe("Link", () => {
let user: UserEvent;

beforeEach(() => {
user = userEvent.setup();
});

it("should render correctly", () => {
const wrapper = render(<Link />);

Expand Down Expand Up @@ -33,6 +40,28 @@ describe("Link", () => {
expect(container.querySelector("svg")).not.toBeNull();
});

it("should trigger onPress function", async () => {
const onPress = jest.fn();
const {getByRole} = render(<Link onPress={onPress} />);

const link = getByRole("link");

await user.click(link);

expect(onPress).toHaveBeenCalled();
});

it("should trigger onClick function", async () => {
const onClick = jest.fn();
const {getByRole} = render(<Link onClick={onClick} />);

const link = getByRole("link");

await user.click(link);

expect(onClick).toHaveBeenCalled();
});

it('should have target="_blank" and rel="noopener noreferrer" when "isExternal" is true', () => {
const {container} = render(
<Link isExternal href="#">
Expand Down
7 changes: 7 additions & 0 deletions packages/components/link/src/use-link.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {AriaLinkProps} from "@react-types/link";
import type {LinkVariantProps} from "@nextui-org/theme";
import type {MouseEventHandler} from "react";

import {link} from "@nextui-org/theme";
import {useAriaLink} from "@nextui-org/use-aria-link";
Expand Down Expand Up @@ -36,6 +37,12 @@ interface Props extends HTMLNextUIProps<"a">, LinkVariantProps {
* @default <LinkIcon />
*/
anchorIcon?: React.ReactNode;
/**
* The native link click event handler.
* use `onPress` instead.
* @deprecated
*/
onClick?: MouseEventHandler<HTMLAnchorElement>;
}

export type UseLinkProps = Props & AriaLinkProps;
Expand Down
29 changes: 28 additions & 1 deletion packages/components/link/stories/link.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {VariantProps} from "@nextui-org/theme";

import {Meta} from "@storybook/react";
import React from "react";
import React, {useState} from "react";
import {tv} from "@nextui-org/theme";
import {link} from "@nextui-org/theme";

Expand Down Expand Up @@ -48,6 +48,22 @@ const defaultProps = {

const Template = (args: LinkProps) => <Link {...args} href="#" />;

const PressableTemplate = (args: LinkProps) => {
const [isOpen, setIsOpen] = useState(false);
const handlePress = (e: any) => {
// eslint-disable-next-line no-console
console.log("Pressed", e);

setIsOpen(!isOpen);
};
Comment on lines +53 to +58
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety and use onPress instead of onClick

The current implementation has several issues:

  1. Uses any type for the event parameter
  2. Uses the deprecated onClick handler
  3. Contains a disabled eslint rule

Consider this improvement:

- const handlePress = (e: any) => {
-   // eslint-disable-next-line no-console
-   console.log("Pressed", e);
-   setIsOpen(!isOpen);
- };
+ const handlePress = (e: PressEvent) => {
+   console.log("Pressed", e);
+   setIsOpen(!isOpen);
+ };

And update the Link usage:

- <Link {...args} onClick={handlePress}>
+ <Link {...args} onPress={handlePress}>

Committable suggestion skipped: line range outside the PR's diff.


return (
<Link {...args} onClick={handlePress}>
{isOpen ? "Open" : "Close"}
</Link>
);
};

export const Default = {
render: Template,

Expand All @@ -59,6 +75,17 @@ export const Default = {
},
};

export const Pressable = {
render: PressableTemplate,

args: {
...defaultProps,
isDisabled: false,
color: "foreground",
size: "md",
},
};

export const Underline = Template.bind({}) as any;
Underline.args = {
...defaultProps,
Expand Down
11 changes: 9 additions & 2 deletions packages/components/listbox/src/base/listbox-item-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,18 @@ interface Props<T extends object = {}> extends Omit<ItemProps<"li", T>, "childre
classNames?: SlotsToClasses<ListboxItemSlots>;
}

export type ListboxItemBaseProps<T extends object = {}> = Props<T> &
export type ListboxItemBaseProps<T extends object = {}> = Omit<Props<T>, "onClick"> &
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace empty object type with a more specific type

Using {} as a type is discouraged as it means "any non-nullable value". Consider defining a more specific interface or type.

-export type ListboxItemBaseProps<T extends object = {}> = Omit<Props<T>, "onClick"> &
+export type ListboxItemBaseProps<T extends Record<string, unknown> = Record<string, unknown>> = Omit<Props<T>, "onClick"> &
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type ListboxItemBaseProps<T extends object = {}> = Omit<Props<T>, "onClick"> &
export type ListboxItemBaseProps<T extends Record<string, unknown> = Record<string, unknown>> = Omit<Props<T>, "onClick"> &
🧰 Tools
🪛 Biome (1.9.4)

[error] 92-92: Don't use '{}' as a type.

Prefer explicitly define the object shape. '{}' means "any non-nullable value".

(lint/complexity/noBannedTypes)

Omit<ListboxItemVariantProps, "hasDescriptionTextChild" | "hasTitleTextChild"> &
Omit<AriaOptionProps, "key"> &
FocusableProps &
PressEvents;
PressEvents & {
/**
* The native click event handler.
* use `onPress` instead.
* @deprecated
*/
onClick?: (e: React.MouseEvent<HTMLLIElement | HTMLAnchorElement>) => void;
};

const ListboxItemBase = BaseItem as <T extends object>(
props: ListboxItemBaseProps<T>,
Expand Down
15 changes: 12 additions & 3 deletions packages/components/listbox/src/use-listbox-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import {useFocusRing} from "@react-aria/focus";
import {Node} from "@react-types/shared";
import {filterDOMProps} from "@nextui-org/react-utils";
import {clsx, dataAttr, objectToDeps, removeEvents} from "@nextui-org/shared-utils";
import {clsx, dataAttr, objectToDeps, removeEvents, warn} from "@nextui-org/shared-utils";
import {useOption} from "@react-aria/listbox";
import {mergeProps} from "@react-aria/utils";
import {useHover, usePress} from "@react-aria/interactions";
Expand Down Expand Up @@ -46,7 +46,7 @@ export function useListboxItem<T extends object>(originalProps: UseListboxItemPr
classNames,
autoFocus,
onPress,
onClick,
onClick: deprecatedOnClick,
shouldHighlightOnFocus,
hideSelectedIcon = false,
isReadOnly = false,
Expand All @@ -68,6 +68,13 @@ export function useListboxItem<T extends object>(originalProps: UseListboxItemPr

const isMobile = useIsMobile();

if (deprecatedOnClick && typeof deprecatedOnClick === "function") {
warn(
"onClick is deprecated, please use onPress instead. See: https://github.com/nextui-org/nextui/issues/4292",
"ListboxItem",
);
}

const {pressProps, isPressed} = usePress({
ref: domRef,
isDisabled: isDisabled,
Expand Down Expand Up @@ -120,7 +127,9 @@ export function useListboxItem<T extends object>(originalProps: UseListboxItemPr
const getItemProps: PropGetter = (props = {}) => ({
ref: domRef,
...mergeProps(
{onClick},
{
onClick: deprecatedOnClick,
},
itemProps,
isReadOnly ? {} : mergeProps(focusProps, pressProps),
hoverProps,
Expand Down
Loading