Compare commits

...

6 Commits

Author SHA1 Message Date
Hayato Hasegawa
b4cfb408e9
fix(autocomplete): show popover when emptyContent is provided with allowsCustomValue (#5951)
* fix(autocomplete): show popover when emptyContent is provided with allowsCustomValue

* chore(autocomplete): remove deprecated story with custom emptyContent and allowsCustomValue

* chore: add changeset
2025-12-01 00:12:59 +08:00
Hayato Hasegawa
66ef76e823
fix(link): resolve issue where useHref is not working correctly (#5933)
* fix(link): correct props merge order in use-link to apply useHref

* chore(link): add changeset

* test(link): adjust href assertion to use getAttribute for compatibility

* fix(link): use routerLinkProps.href in handleLinkClick for consistency

* fix(link): process href with useHref before passing to useLinkProps

* test(link): comment out useHref provider test

* Revert "test(link): comment out useHref provider test"

This reverts commit de053f835fa57becf6e807a64c17339fc61d8689.

* Revert "fix(link): process href with useHref before passing to useLinkProps"

This reverts commit 81e5f7b8d1ea7002b9718cf521b5c6f2af79ac42.

* fix(provider): add `useHref` to provider context and integrate with `useLink`

* test(link): comment out useHref provider test

* fix(link): correct href handling in link component and update tests

* fix(link): simplify props handling by removing resolvedProps logic and reorder mergeProps usage

* fix(provider): remove `useHref` from provider context and related logic

* refactor(link): add spacing

* chore(changeset): add issue numbers

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>
2025-11-28 18:34:17 +08:00
WK
0825f88cd2
fix(spinner): cater global spinner variant (#5948)
* fix(spinner): cater global spinner variant

* feat(spinner): add spinner test cases

* chore(changeset): add changeset
2025-11-26 16:46:34 +08:00
WK
ce0c298785
fix(number-input): lable position for empty percent format (#5945)
* fix(number-input): lable position for empty percent format

* chore(changeset): fix typo
2025-11-26 13:56:48 +08:00
WK
4fa54534b2
fix(radio): handle props styles in getBaseProps (#5944)
* fix(radio): handle props styles in getBaseProps

* refactor(radio): examples
2025-11-26 13:55:37 +08:00
WK
484212712c
chore(deps): bump posthog-js (#5937) 2025-11-25 10:41:59 +08:00
18 changed files with 170 additions and 43 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/autocomplete": patch
---
show popover when emptyContent is provided with allowsCustomValue (#5745)

View File

@ -0,0 +1,5 @@
---
"@heroui/number-input": patch
---
fix label position for empty percent format (#5941)

View File

@ -0,0 +1,5 @@
---
"@heroui/spinner": patch
---
cater global spinner variant (#5939)

View File

@ -0,0 +1,5 @@
---
"@heroui/link": patch
---
Fix an issue where the useHref was not being applied correctly (#5925, #5431, #5600)

View File

@ -0,0 +1,5 @@
---
"@heroui/radio": patch
---
handle props styles in getBaseProps (#5942)

View File

@ -15,12 +15,13 @@ export const CustomRadio = (props) => {
return ( return (
<Component <Component
{...getBaseProps()} {...getBaseProps({
className={cn( className: cn(
"group inline-flex items-center hover:opacity-70 active:opacity-50 justify-between flex-row-reverse tap-highlight-transparent", "group inline-flex items-center hover:opacity-70 active:opacity-50 justify-between flex-row-reverse tap-highlight-transparent m-0",
"max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4", "max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4",
"data-[selected=true]:border-primary", "data-[selected=true]:border-primary",
)} ),
})}
> >
<VisuallyHidden> <VisuallyHidden>
<input {...getInputProps()} /> <input {...getInputProps()} />

View File

@ -18,12 +18,13 @@ export const CustomRadio = (props: RadioProps) => {
return ( return (
<Component <Component
{...getBaseProps()} {...getBaseProps({
className={cn( className: cn(
"group inline-flex items-center justify-between hover:bg-content2 flex-row-reverse", "group inline-flex items-center hover:opacity-70 active:opacity-50 justify-between flex-row-reverse tap-highlight-transparent m-0",
"max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4", "max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4",
"data-[selected=true]:border-primary", "data-[selected=true]:border-primary",
)} ),
})}
> >
<VisuallyHidden> <VisuallyHidden>
<input {...getInputProps()} /> <input {...getInputProps()} />

View File

@ -62,7 +62,7 @@
"next-contentlayer2": "0.5.8", "next-contentlayer2": "0.5.8",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"parse-numeric-range": "1.2.0", "parse-numeric-range": "1.2.0",
"posthog-js": "1.197.0", "posthog-js": "1.298.0",
"prism-react-renderer": "^1.2.1", "prism-react-renderer": "^1.2.1",
"react": "18.3.0", "react": "18.3.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",

View File

@ -3,7 +3,7 @@ import type {UserEvent} from "@testing-library/user-event";
import type {AutocompleteProps} from "../src"; import type {AutocompleteProps} from "../src";
import * as React from "react"; import * as React from "react";
import {within, render, renderHook, act} from "@testing-library/react"; import {within, render, renderHook, act, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import {spy, shouldIgnoreReactWarning} from "@heroui/test-utils"; import {spy, shouldIgnoreReactWarning} from "@heroui/test-utils";
import {useForm} from "react-hook-form"; import {useForm} from "react-hook-form";
@ -1089,3 +1089,63 @@ describe("focusedKey management with selected key", () => {
expect(optionItem).toHaveAttribute("data-focus", "true"); expect(optionItem).toHaveAttribute("data-focus", "true");
}); });
}); });
describe("Autocomplete with allowsCustomValue", () => {
let user: UserEvent;
beforeEach(() => {
user = userEvent.setup();
});
it("should show the empty content when allowsCustomValue is true and a custom emptyContent is provided", async () => {
const wrapper = render(
<Autocomplete
allowsCustomValue
aria-label="Favorite Animal"
data-testid="autocomplete"
defaultItems={[]}
label="Favorite Animal"
listboxProps={{
emptyContent: <div data-testid="empty-content">No animals found</div>,
}}
>
{(item: Item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>,
);
const input = wrapper.getByTestId("autocomplete");
await user.click(input);
act(() => {
jest.runAllTimers();
});
const emptyContent = wrapper.getByTestId("empty-content");
await waitFor(() => {
expect(emptyContent).toBeVisible();
});
});
it("should not show the empty content when allowsCustomValue is true and no custom emptyContent is provided", async () => {
const wrapper = render(
<Autocomplete
allowsCustomValue
aria-label="Favorite Animal"
defaultItems={[]}
label="Favorite Animal"
>
{(item: Item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>,
);
const input = wrapper.getByRole("combobox");
await user.click(input);
const listbox = wrapper.queryByRole("listbox");
expect(listbox).toBeNull();
});
});

View File

@ -297,7 +297,7 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
), ),
listboxProps: mergeProps( listboxProps: mergeProps(
{ {
hideEmptyContent: allowsCustomValue, hideEmptyContent: allowsCustomValue && !listboxProps?.emptyContent,
emptyContent: "No results found.", emptyContent: "No results found.",
disableAnimation, disableAnimation,
}, },

View File

@ -3,6 +3,7 @@ import type {UserEvent} from "@testing-library/user-event";
import * as React from "react"; import * as React from "react";
import {render} from "@testing-library/react"; import {render} from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import {HeroUIProvider} from "@heroui/system";
import {Link} from "../src"; import {Link} from "../src";
@ -84,4 +85,17 @@ describe("Link", () => {
expect(container.querySelector("button")?.getAttribute("role")).toBe("link"); expect(container.querySelector("button")?.getAttribute("role")).toBe("link");
}); });
it("should apply useHref from provider", () => {
const useHref = (href: string) => `/example${href}`;
const {getByRole} = render(
<HeroUIProvider navigate={jest.fn()} useHref={useHref}>
<Link href="/test">Test Link</Link>
</HeroUIProvider>,
);
const link = getByRole("link");
expect(link.getAttribute("href")).toBe("/example/test");
});
}); });

View File

@ -110,7 +110,7 @@ export function useLink(originalProps: UseLinkProps) {
"data-focus": dataAttr(isFocused), "data-focus": dataAttr(isFocused),
"data-disabled": dataAttr(originalProps.isDisabled), "data-disabled": dataAttr(originalProps.isDisabled),
"data-focus-visible": dataAttr(isFocusVisible), "data-focus-visible": dataAttr(isFocusVisible),
...mergeProps(focusProps, linkProps, otherProps), ...mergeProps(focusProps, otherProps, linkProps),
}; };
}, [styles, isFocused, isFocusVisible, focusProps, linkProps, otherProps]); }, [styles, isFocused, isFocusVisible, focusProps, linkProps, otherProps]);

View File

@ -155,7 +155,7 @@ export function useNumberInput(originalProps: UseNumberInputProps) {
const inputValue = isNaN(state.numberValue) ? "" : state.numberValue; const inputValue = isNaN(state.numberValue) ? "" : state.numberValue;
const isFilled = !isEmpty(inputValue); const isFilled = !isEmpty(state.inputValue) && !isEmpty(inputValue);
const isFilledWithin = isFilled || isFocusWithin; const isFilledWithin = isFilled || isFocusWithin;

View File

@ -156,7 +156,7 @@ export function useRadio(props: UseRadioProps) {
return { return {
...props, ...props,
ref: domRef, ref: domRef,
className: slots.base({class: baseStyles}), className: slots.base({class: clsx(baseStyles, props?.className)}),
"data-disabled": dataAttr(isDisabled), "data-disabled": dataAttr(isDisabled),
"data-focus": dataAttr(isFocused), "data-focus": dataAttr(isFocused),
"data-focus-visible": dataAttr(isFocusVisible), "data-focus-visible": dataAttr(isFocusVisible),

View File

@ -4,11 +4,11 @@ import type {RadioProps, RadioGroupProps} from "../src";
import React from "react"; import React from "react";
import {VisuallyHidden} from "@react-aria/visually-hidden"; import {VisuallyHidden} from "@react-aria/visually-hidden";
import {radio, button} from "@heroui/theme"; import {radio, button, cn} from "@heroui/theme";
import {clsx} from "@heroui/shared-utils"; import {clsx} from "@heroui/shared-utils";
import {Form} from "@heroui/form"; import {Form} from "@heroui/form";
import {RadioGroup, Radio, useRadio, useRadioGroupContext} from "../src"; import {RadioGroup, Radio, useRadio} from "../src";
export default { export default {
title: "Components/RadioGroup", title: "Components/RadioGroup",
@ -360,19 +360,14 @@ export const Controlled = {
const CustomRadio = (props: RadioProps) => { const CustomRadio = (props: RadioProps) => {
const {children, ...otherProps} = props; const {children, ...otherProps} = props;
const {groupState} = useRadioGroupContext();
const isSelected = groupState.selectedValue === otherProps.value;
return ( return (
<Radio <Radio
{...otherProps} {...otherProps}
classNames={{ classNames={{
base: clsx( base: cn(
"inline-flex bg-content1 hover:bg-content2 items-center justify-between flex-row-reverse max-w-[300px] cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent", "inline-flex m-0 bg-content1 hover:bg-content2 items-center justify-between",
{ "flex-row-reverse max-w-[300px] cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent",
"border-primary": isSelected, "data-[selected=true]:border-primary",
},
), ),
}} }}
> >
@ -401,7 +396,6 @@ const RadioCard = (props: RadioProps) => {
const { const {
Component, Component,
children, children,
isSelected,
description, description,
getBaseProps, getBaseProps,
getWrapperProps, getWrapperProps,
@ -413,13 +407,13 @@ const RadioCard = (props: RadioProps) => {
return ( return (
<Component <Component
{...getBaseProps()} {...getBaseProps({
className={clsx( className: cn(
"group inline-flex items-center justify-between hover:bg-content2 flex-row-reverse max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4", "group inline-flex items-center hover:opacity-70 active:opacity-50 justify-between flex-row-reverse tap-highlight-transparent m-0",
{ "max-w-[300px] cursor-pointer border-2 border-default rounded-lg gap-4 p-4",
"border-primary": isSelected, "data-[selected=true]:border-primary",
}, ),
)} })}
> >
<VisuallyHidden> <VisuallyHidden>
<input {...getInputProps()} /> <input {...getInputProps()} />

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import {render} from "@testing-library/react"; import {render} from "@testing-library/react";
import {HeroUIProvider} from "@heroui/system";
import {Spinner} from "../src"; import {Spinner} from "../src";
@ -40,4 +41,24 @@ describe("Spinner", () => {
expect(getByLabelText("Custom label")).toBeInTheDocument(); expect(getByLabelText("Custom label")).toBeInTheDocument();
}); });
it("should use global spinner variant if variant is not defined", () => {
const {container} = render(
<HeroUIProvider spinnerVariant="gradient">
<Spinner aria-label="Custom label" />
</HeroUIProvider>,
);
expect(container.querySelector("[class*='gradient']")).toBeInTheDocument();
});
it("should not use global spinner variant if variant is defined", () => {
const {container} = render(
<HeroUIProvider spinnerVariant="gradient">
<Spinner aria-label="Custom label" variant="default" />
</HeroUIProvider>,
);
expect(container.querySelector("[class*='gradient']")).not.toBeInTheDocument();
});
}); });

View File

@ -45,7 +45,10 @@ export function useSpinner(originalProps: UseSpinnerProps) {
const {children, className, classNames, label: labelProp, ...otherProps} = props; const {children, className, classNames, label: labelProp, ...otherProps} = props;
const slots = useMemo(() => spinner({...variantProps}), [objectToDeps(variantProps)]); const slots = useMemo(
() => spinner({...variantProps, variant}),
[objectToDeps(variantProps), variant],
);
const baseStyles = clsx(classNames?.base, className); const baseStyles = clsx(classNames?.base, className);

18
pnpm-lock.yaml generated
View File

@ -413,8 +413,8 @@ importers:
specifier: 1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
posthog-js: posthog-js:
specifier: 1.197.0 specifier: 1.298.0
version: 1.197.0 version: 1.298.0
prism-react-renderer: prism-react-renderer:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.3.5(react@18.3.0) version: 1.3.5(react@18.3.0)
@ -6715,6 +6715,9 @@ packages:
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.6.0':
resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==}
'@protobufjs/aspromise@1.1.2': '@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@ -13144,8 +13147,8 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
posthog-js@1.197.0: posthog-js@1.298.0:
resolution: {integrity: sha512-tvzx47x/vn/XoDTrZsn2f1WbswuwnesDmihdMpbTuxHapfNH4Jp7aV3XCs6hT0Qujo4MNG3nB/kap33FpWatDA==} resolution: {integrity: sha512-Zwzsf7TO8qJ6DFLuUlQSsT/5OIOcxSBZlKOSk3satkEnwKdmnBXUuxgVXRHrvq1kj7OB2PVAPgZiQ8iHHj9DRA==}
preact@10.26.9: preact@10.26.9:
resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==}
@ -18758,6 +18761,10 @@ snapshots:
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@posthog/core@1.6.0':
dependencies:
cross-spawn: 7.0.6
'@protobufjs/aspromise@1.1.2': {} '@protobufjs/aspromise@1.1.2': {}
'@protobufjs/base64@1.1.2': {} '@protobufjs/base64@1.1.2': {}
@ -26834,8 +26841,9 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
posthog-js@1.197.0: posthog-js@1.298.0:
dependencies: dependencies:
'@posthog/core': 1.6.0
core-js: 3.43.0 core-js: 3.43.0
fflate: 0.4.8 fflate: 0.4.8
preact: 10.26.9 preact: 10.26.9