From f5d622ed46aa28ea6456d28ba04b4ef0c92580bb Mon Sep 17 00:00:00 2001 From: doki- <1335902682@qq.com> Date: Fri, 10 Oct 2025 16:59:21 +0800 Subject: [PATCH 01/17] fix(extendVariants): return component type error --- .changeset/rich-horses-dress.md | 5 +++++ packages/core/system-rsc/src/extend-variants.d.ts | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/rich-horses-dress.md diff --git a/.changeset/rich-horses-dress.md b/.changeset/rich-horses-dress.md new file mode 100644 index 0000000000..8847a92f7a --- /dev/null +++ b/.changeset/rich-horses-dress.md @@ -0,0 +1,5 @@ +--- +"@heroui/system-rsc": patch +--- + +fix extendVariants return type error(#5778) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index 2dceadae6d..afd764b77f 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -103,11 +103,11 @@ export type ExtendVariants = { }, opts?: Options, ): ForwardRefExoticComponent< - PropsWithoutRef< - CP & { - [key in keyof V]?: StringToBoolean; - } - > & + PropsWithoutRef<{ + [key in keyof CP | keyof V]?: + | (key extends keyof CP ? CP[key] : never) + | (key extends keyof V ? StringToBoolean : never); + }> & RefAttributes> >; }; From 3858ffd80b001bd23aec6d4fcde7789a8f9241bc Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Fri, 24 Oct 2025 23:26:46 +0200 Subject: [PATCH 02/17] fix(CompoundVariants): correct type inference for extended/compound variants and composition --- .changeset/rich-horses-dress.md | 5 ----- packages/core/system-rsc/src/extend-variants.d.ts | 7 +++++-- 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 .changeset/rich-horses-dress.md diff --git a/.changeset/rich-horses-dress.md b/.changeset/rich-horses-dress.md deleted file mode 100644 index 8847a92f7a..0000000000 --- a/.changeset/rich-horses-dress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@heroui/system-rsc": patch ---- - -fix extendVariants return type error(#5778) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index afd764b77f..d30e023015 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -48,7 +48,10 @@ type VariantValue = { type DefaultVariants = VariantValue; -type CompoundVariants = Array & ClassProp>; +type CompoundVariants = Array< + VariantValue & + ClassProp> +>; type Options = { /** @@ -92,7 +95,7 @@ export type ExtendVariants = { V extends ComposeVariants, SV extends SuggestedVariants, DV extends DefaultVariants, - CV extends CompoundVariants, + CV extends CompoundVariants>, >( BaseComponent: C, styles: { From 84aaa3e58bd876e67000385b6e2a26f0754ff4f5 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Fri, 24 Oct 2025 23:57:56 +0200 Subject: [PATCH 03/17] test: cover compound/extend inference; enforce CP required props --- .../__tests__/extend-variants.test.tsx | 16 +++++++++++++--- .../core/system-rsc/src/extend-variants.d.ts | 12 +++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index 7b139f418c..6bb0544df2 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -1,4 +1,4 @@ -import type {ExtendVariantProps} from "../src/extend-variants"; +import type {ExtendVariantProps, ExtendVariantWithSlotsProps} from "../src/extend-variants"; import React from "react"; import {render, screen} from "@testing-library/react"; @@ -37,7 +37,7 @@ const createExtendNoSlotsComponent = (styles: ExtendVariantProps = {}) => ], }); -const createExtendSlotsComponent = () => +const createExtendSlotsComponent = (styles: ExtendVariantWithSlotsProps = {}) => extendVariants(Card, { variants: { shadow: { @@ -73,6 +73,7 @@ const createExtendSlotsComponent = () => shadow: "xl", radius: "xl", }, + compoundVariants: styles?.compoundVariants ?? [], }); describe("extendVariants function - no slots", () => { @@ -242,7 +243,16 @@ describe("extendVariants function - with slots", () => { }); test("should override all slots styles", () => { - const Card2 = createExtendSlotsComponent(); + const Card2 = createExtendSlotsComponent({ + compoundVariants: [ + { + fullWidth: "true", + class: { + body: "w-full", + }, + }, + ], + }); const {getByTestId} = render( Card Content, ); diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index d30e023015..a4d5bb088c 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -106,11 +106,13 @@ export type ExtendVariants = { }, opts?: Options, ): ForwardRefExoticComponent< - PropsWithoutRef<{ - [key in keyof CP | keyof V]?: - | (key extends keyof CP ? CP[key] : never) - | (key extends keyof V ? StringToBoolean : never); - }> & + PropsWithoutRef< + CP & { + [K in Exclude]?: StringToBoolean; + } & { + [K in Extract]: CP[K] | StringToBoolean; + } + > & RefAttributes> >; }; From 5ab2c681709e9a8a802edabf338d08b47bb5953a Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 00:46:48 +0200 Subject: [PATCH 04/17] fix(types): correct CompoundVariants class value inference Replaces the conditional ClassProp logic with a simpler, consistent form to fix incorrect slot value inference. Before: ClassProp> After: ClassProp> This ensures GetSuggestedValues is used for slot-aware variants and avoids duplicated conditional branches for undefined slots. --- packages/core/system-rsc/src/extend-variants.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index a4d5bb088c..5932de75cf 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -49,8 +49,7 @@ type VariantValue = { type DefaultVariants = VariantValue; type CompoundVariants = Array< - VariantValue & - ClassProp> + VariantValue & ClassProp> >; type Options = { From 2dfa44cc2a415d06dd9fe34dfe2703308db1abd1 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 00:50:03 +0200 Subject: [PATCH 05/17] fix(system-rsc): correct slot detection in getSlots() --- .../core/system-rsc/src/extend-variants.js | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.js b/packages/core/system-rsc/src/extend-variants.js index f91957acab..16e3d7cea5 100644 --- a/packages/core/system-rsc/src/extend-variants.js +++ b/packages/core/system-rsc/src/extend-variants.js @@ -5,21 +5,32 @@ import clsx from "clsx"; import {mapPropsVariants} from "./utils"; function getSlots(variants) { - return variants - ? Object.values(variants) - .flatMap(Object.values) - .reduce((acc, slot) => { - if (typeof slot === "object" && slot !== null && !(slot instanceof String)) { - Object.keys(slot).forEach((key) => { - if (!acc.hasOwnProperty(key)) { - acc[key] = ""; - } - }); - } - - return acc; - }, {}) - : {}; + if (!variants || typeof variants !== "object") return {}; + + const acc = Object.create(null); + + for (const group of Object.values(variants)) { + if (!group || typeof group !== "object") continue; + + for (const config of Object.values(group)) { + if ( + !config || + typeof config !== "object" || + Array.isArray(config) || + config instanceof String + ) { + continue; + } + + for (const slotName of Object.keys(config)) { + if (!Object.prototype.hasOwnProperty.call(acc, slotName)) { + acc[slotName] = ""; + } + } + } + } + + return acc; } function getClassNamesWithProps({ From 6336fa8767ff40f566c477d6222c2fb8b5f154b2 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 02:27:27 +0200 Subject: [PATCH 06/17] fix(types): make ExtendVariants props optional and guard V[key] with NonNullable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifies the return type of ExtendVariants to ensure no required props are enforced at the HOC level. This aligns with the intended API contract where extended components expose all props as optional. - All keys (CP ∪ V) are optional - Preserve CP type hints and booleanized V values - Added NonNullable guard to prevent undefined indexing --- packages/core/system-rsc/src/extend-variants.d.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index 5932de75cf..dc96580c4a 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -105,13 +105,11 @@ export type ExtendVariants = { }, opts?: Options, ): ForwardRefExoticComponent< - PropsWithoutRef< - CP & { - [K in Exclude]?: StringToBoolean; - } & { - [K in Extract]: CP[K] | StringToBoolean; - } - > & + PropsWithoutRef<{ + [key in keyof CP | keyof V]?: + | (key extends keyof CP ? CP[key] : never) + | (key extends keyof V ? StringToBoolean> : never); + }> & RefAttributes> >; }; From b770b54c505ae2e50edaa0a25ca23735f83fa34a Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 03:32:35 +0200 Subject: [PATCH 07/17] test(extendVariants): add compoundVariants integration test --- .../__tests__/extend-variants.test.tsx | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index 6bb0544df2..b49b038406 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -73,7 +73,13 @@ const createExtendSlotsComponent = (styles: ExtendVariantWithSlotsProps = {}) => shadow: "xl", radius: "xl", }, - compoundVariants: styles?.compoundVariants ?? [], + compoundVariants: styles?.compoundVariants ?? [ + { + shadow: "none", + radius: "md", + class: "scale-150", + }, + ], }); describe("extendVariants function - no slots", () => { @@ -243,24 +249,45 @@ describe("extendVariants function - with slots", () => { }); test("should override all slots styles", () => { + const Card2 = createExtendSlotsComponent(); + const {getByTestId} = render( + Card Content, + ); + + const baseEl = getByTestId("base"); + const headerEl = getByTestId("header"); + + expect(baseEl).toHaveClass("shadow-xs"); + expect(headerEl).toHaveClass("rounded-none"); + }); + + test("should include the compound variant styles - original", () => { const Card2 = createExtendSlotsComponent({ compoundVariants: [ { - fullWidth: "true", + shadow: "none", + radius: "md", + class: "scale-150", + }, + { + radius: "md", class: { - body: "w-full", + header: "rounded-xl", }, }, ], }); + const {getByTestId} = render( - Card Content, + + Card Content + , ); const baseEl = getByTestId("base"); const headerEl = getByTestId("header"); - expect(baseEl).toHaveClass("shadow-xs"); - expect(headerEl).toHaveClass("rounded-none"); + expect(baseEl).toHaveClass("scale-150"); + expect(headerEl).toHaveClass("rounded-xl"); }); }); From 029355241db85efa5e5758170bf6e85b165b048c Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 17:34:53 +0200 Subject: [PATCH 08/17] fix(system-rsc): getSlots() brief JSDoc comment added --- packages/core/system-rsc/src/extend-variants.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/core/system-rsc/src/extend-variants.js b/packages/core/system-rsc/src/extend-variants.js index 16e3d7cea5..a199400e1f 100644 --- a/packages/core/system-rsc/src/extend-variants.js +++ b/packages/core/system-rsc/src/extend-variants.js @@ -4,6 +4,12 @@ import clsx from "clsx"; import {mapPropsVariants} from "./utils"; +/** + * Extracts slot names from variant configurations. + * Traverses: variants -> variant groups -> variant configs -> slot names + * @param {Object} variants - Nested object: { variantName: { value: { slotName: "...", ... } } } + * @returns {Object} Map of slot names to empty strings + */ function getSlots(variants) { if (!variants || typeof variants !== "object") return {}; @@ -13,6 +19,7 @@ function getSlots(variants) { if (!group || typeof group !== "object") continue; for (const config of Object.values(group)) { + // Skip non-objects, arrays (which would yield numeric indices), and String objects if ( !config || typeof config !== "object" || From 0021c8fbb6c9e2c878fe374c1b7ea3e35854c67f Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 17:51:55 +0200 Subject: [PATCH 09/17] test(extendVariants): new styles - extended & fixed styles - original tests for slots component --- .../__tests__/extend-variants.test.tsx | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index b49b038406..5381843cd2 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -76,8 +76,14 @@ const createExtendSlotsComponent = (styles: ExtendVariantWithSlotsProps = {}) => compoundVariants: styles?.compoundVariants ?? [ { shadow: "none", - radius: "md", - class: "scale-150", + radius: "none", + class: "rounded-sm", + }, + { + shadow: "none", + class: { + header: "scale-75", + }, }, ], }); @@ -261,25 +267,41 @@ describe("extendVariants function - with slots", () => { expect(headerEl).toHaveClass("rounded-none"); }); + test("should include the compound variant styles - extended", () => { + const Card2 = createExtendSlotsComponent(); + + const {getByTestId} = render( + + Card Content + , + ); + + const baseEl = getByTestId("base"); + const headerEl = getByTestId("header"); + + expect(baseEl).toHaveClass("rounded-sm"); + expect(headerEl).toHaveClass("scale-75"); + }); + test("should include the compound variant styles - original", () => { const Card2 = createExtendSlotsComponent({ compoundVariants: [ { shadow: "none", - radius: "md", - class: "scale-150", + radius: "sm", + class: "rounded-xl", }, { - radius: "md", + radius: "sm", class: { - header: "rounded-xl", + header: "scale-150", }, }, ], }); const {getByTestId} = render( - + Card Content , ); @@ -287,7 +309,7 @@ describe("extendVariants function - with slots", () => { const baseEl = getByTestId("base"); const headerEl = getByTestId("header"); - expect(baseEl).toHaveClass("scale-150"); - expect(headerEl).toHaveClass("rounded-xl"); + expect(baseEl).toHaveClass("rounded-xl"); + expect(headerEl).toHaveClass("scale-150"); }); }); From 73032ce4bce0a1a7d82797674db10d487cbb80db Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sat, 25 Oct 2025 18:10:33 +0200 Subject: [PATCH 10/17] test(extendVariants): fixed slot component variant styles extended test --- packages/core/system-rsc/__tests__/extend-variants.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index 5381843cd2..3c35ae21e5 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -271,7 +271,7 @@ describe("extendVariants function - with slots", () => { const Card2 = createExtendSlotsComponent(); const {getByTestId} = render( - + Card Content , ); From 8baeab2820a321e1b43b77ede66ce4bb4de646b2 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sun, 26 Oct 2025 12:03:19 +0100 Subject: [PATCH 11/17] fix(types): avoid leaking React internals by removing PropsWithoutRef MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace PropsWithoutRef with explicit Exclude<'ref'> in mapped keys and intersect with RefAttributes>. This prevents @types/react’s internal UNDEFINED_VOID_ONLY from leaking into the public .d.ts and fixes declaration emit for components like extended Autocomplete. --- packages/core/system-rsc/src/extend-variants.d.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index dc96580c4a..b6a4d4ec34 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -1,10 +1,5 @@ import type {ClassValue, StringToBoolean, OmitUndefined, ClassProp} from "tailwind-variants"; -import type { - ForwardRefExoticComponent, - JSXElementConstructor, - PropsWithoutRef, - RefAttributes, -} from "react"; +import type {ForwardRefExoticComponent, JSXElementConstructor, RefAttributes} from "react"; type SlotsClassValue = { [K in keyof S]?: ClassValue; @@ -105,12 +100,11 @@ export type ExtendVariants = { }, opts?: Options, ): ForwardRefExoticComponent< - PropsWithoutRef<{ - [key in keyof CP | keyof V]?: + { + [key in Exclude]?: | (key extends keyof CP ? CP[key] : never) | (key extends keyof V ? StringToBoolean> : never); - }> & - RefAttributes> + } & RefAttributes> >; }; From 4e64ece28d86d30598cc4268f32f2a0c381f4e26 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sun, 26 Oct 2025 14:48:32 +0100 Subject: [PATCH 12/17] chore(changeset): add patch for extendVariants and CompoundVariants type fix --- .changeset/nine-apes-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nine-apes-remain.md diff --git a/.changeset/nine-apes-remain.md b/.changeset/nine-apes-remain.md new file mode 100644 index 0000000000..cdb67ef6ea --- /dev/null +++ b/.changeset/nine-apes-remain.md @@ -0,0 +1,5 @@ +--- +"@heroui/system-rsc": major +--- + +fix(system-rsc): correct type inference in extendVariants and CompoundVariants From c878267fd37f3a4d013c8c8be13881661ec2f6e5 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sun, 26 Oct 2025 14:55:22 +0100 Subject: [PATCH 13/17] chore(system-rsc): add changeset for getSlots() slot detection fix --- .changeset/four-guests-sneeze.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-guests-sneeze.md diff --git a/.changeset/four-guests-sneeze.md b/.changeset/four-guests-sneeze.md new file mode 100644 index 0000000000..c6f4661953 --- /dev/null +++ b/.changeset/four-guests-sneeze.md @@ -0,0 +1,5 @@ +--- +"@heroui/system-rsc": minor +--- + +fix(system-rsc): correct slot detection in getSlots() to ensure proper slot key extraction and consistent compoundVariants behavior. From d18110fcfd5902f8b03a892af9435f163df73e43 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Sun, 26 Oct 2025 16:59:03 +0100 Subject: [PATCH 14/17] refactor(types): unify slot value inference via GetSuggestedValues for consistent variant typing --- packages/core/system-rsc/src/extend-variants.d.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index b6a4d4ec34..e9e0d6b2dc 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -6,7 +6,7 @@ type SlotsClassValue = { }; type Variants = { - [K: string]: {[P: string]: S extends undefined ? ClassValue : SlotsClassValue}; + [K: string]: {[P: string]: GetSuggestedValues}; }; type ComponentProps = C extends JSXElementConstructor ? P : never; @@ -15,7 +15,7 @@ type ComponentSlots = CP extends {classNames?: infer S} ? S : undefined; type ValidateSubtype = OmitUndefined extends U ? "true" : "false"; -type GetSuggestedValues = S extends undefined ? ClassValue : SlotsClassValue; +type GetSuggestedValues = ClassValue | (S extends undefined ? never : SlotsClassValue); type SuggestedVariants = { [K in keyof CP]?: ValidateSubtype extends "true" @@ -43,9 +43,7 @@ type VariantValue = { type DefaultVariants = VariantValue; -type CompoundVariants = Array< - VariantValue & ClassProp> ->; +type CompoundVariants = Array & ClassProp>>; type Options = { /** From 9f38de727a5383bd549e98fd4f3baf792bd809d7 Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Wed, 26 Nov 2025 22:36:51 +0100 Subject: [PATCH 15/17] fix(extendVariants): improved as-prop handling and exclude classNames from SuggestedVariants --- .../core/system-rsc/src/extend-variants.d.ts | 72 ++++++++++--------- .../core/system-rsc/src/extend-variants.js | 4 +- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index e9e0d6b2dc..6a00073603 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -1,5 +1,6 @@ import type {ClassValue, StringToBoolean, OmitUndefined, ClassProp} from "tailwind-variants"; import type {ForwardRefExoticComponent, JSXElementConstructor, RefAttributes} from "react"; +import type * as _heroui_system from "@heroui/system"; type SlotsClassValue = { [K in keyof S]?: ClassValue; @@ -18,13 +19,13 @@ type ValidateSubtype = OmitUndefined extends U ? "true" : "false"; type GetSuggestedValues = ClassValue | (S extends undefined ? never : SlotsClassValue); type SuggestedVariants = { - [K in keyof CP]?: ValidateSubtype extends "true" + [K in Exclude]?: ValidateSubtype< + CP[K], + string + > extends "true" ? {[K2 in CP[K]]?: GetSuggestedValues} : ValidateSubtype extends "true" - ? { - true?: GetSuggestedValues; - false?: GetSuggestedValues; - } + ? {true?: GetSuggestedValues; false?: GetSuggestedValues} : never; }; @@ -41,7 +42,9 @@ type VariantValue = { : never); }; -type DefaultVariants = VariantValue; +type DefaultVariants = VariantValue & { + classNames?: S extends undefined ? never : SlotsClassValue; +}; type CompoundVariants = Array & ClassProp>>; @@ -68,7 +71,7 @@ export type ExtendVariantProps = { export type ExtendVariantWithSlotsProps = { variants?: Record>>; - defaultVariants?: Record; + defaultVariants?: Record>; compoundVariants?: Array>>; }; @@ -79,32 +82,35 @@ type InferRef = ? I : any; -export type ExtendVariants = { - < - C extends JSXElementConstructor, - CP extends ComponentProps, - S extends ComponentSlots, - V extends ComposeVariants, - SV extends SuggestedVariants, - DV extends DefaultVariants, - CV extends CompoundVariants>, - >( - BaseComponent: C, - styles: { - variants?: V; - defaultVariants?: DV; - compoundVariants?: CV; - slots?: S; - }, - opts?: Options, - ): ForwardRefExoticComponent< - { - [key in Exclude]?: - | (key extends keyof CP ? CP[key] : never) - | (key extends keyof V ? StringToBoolean> : never); - } & RefAttributes> - >; -}; +export type ExtendVariants = < + C extends keyof JSX.IntrinsicElements | JSXElementConstructor, + CP extends ComponentProps = ComponentProps, + S extends ComponentSlots = ComponentSlots, + V extends ComposeVariants = ComposeVariants, + SV extends SuggestedVariants = SuggestedVariants, + DV extends DefaultVariants = DefaultVariants, + CV extends CompoundVariants> = CompoundVariants< + V, + SV, + ComponentSlots + >, +>( + BaseComponent: C, + styles: { + variants?: V; + defaultVariants?: DV; + compoundVariants?: CV; + slots?: S; + }, + opts?: Options, +) => _heroui_system.InternalForwardRefRenderFunction< + C, + { + [K in Exclude]?: + | (K extends keyof CP ? CP[K] : never) + | (K extends keyof V ? StringToBoolean> : never); + } +>; // main function export declare const extendVariants: ExtendVariants; diff --git a/packages/core/system-rsc/src/extend-variants.js b/packages/core/system-rsc/src/extend-variants.js index a199400e1f..232c05be1f 100644 --- a/packages/core/system-rsc/src/extend-variants.js +++ b/packages/core/system-rsc/src/extend-variants.js @@ -30,9 +30,7 @@ function getSlots(variants) { } for (const slotName of Object.keys(config)) { - if (!Object.prototype.hasOwnProperty.call(acc, slotName)) { - acc[slotName] = ""; - } + acc[slotName] = ""; } } } From fe87bdb4fd212c4fd7008c4c7eb5304ecbaba1ac Mon Sep 17 00:00:00 2001 From: ITBoomBKStudio Date: Wed, 31 Dec 2025 17:53:56 +0100 Subject: [PATCH 16/17] fix(system-rsc): add polymorphic 'as' prop support to extendVariants --- .changeset/four-guests-sneeze.md | 5 - .changeset/nine-apes-remain.md | 4 +- .../__tests__/extend-variants.test.tsx | 101 +++++++++++++++++- .../core/system-rsc/src/extend-variants.d.ts | 4 +- .../core/system-rsc/src/extend-variants.js | 9 +- packages/core/system-rsc/src/types.ts | 2 +- 6 files changed, 110 insertions(+), 15 deletions(-) delete mode 100644 .changeset/four-guests-sneeze.md diff --git a/.changeset/four-guests-sneeze.md b/.changeset/four-guests-sneeze.md deleted file mode 100644 index c6f4661953..0000000000 --- a/.changeset/four-guests-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@heroui/system-rsc": minor ---- - -fix(system-rsc): correct slot detection in getSlots() to ensure proper slot key extraction and consistent compoundVariants behavior. diff --git a/.changeset/nine-apes-remain.md b/.changeset/nine-apes-remain.md index cdb67ef6ea..ca670ffb6d 100644 --- a/.changeset/nine-apes-remain.md +++ b/.changeset/nine-apes-remain.md @@ -1,5 +1,5 @@ --- -"@heroui/system-rsc": major +"@heroui/system-rsc": patch --- -fix(system-rsc): correct type inference in extendVariants and CompoundVariants +fix(system-rsc): correct type inference in extendVariants and CompoundVariants & correct slot detection in getSlots() to ensure proper slot key extraction and consistent compoundVariants behavior. diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index 3c35ae21e5..da9f0cf457 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -6,6 +6,7 @@ import {render, screen} from "@testing-library/react"; import {extendVariants} from "../src/extend-variants"; import {Button} from "../test-utils/extend-components"; import {Card} from "../test-utils/slots-component"; +import {Link} from "../../react/src"; const createExtendNoSlotsComponent = (styles: ExtendVariantProps = {}) => extendVariants(Button, { @@ -40,6 +41,11 @@ const createExtendNoSlotsComponent = (styles: ExtendVariantProps = {}) => const createExtendSlotsComponent = (styles: ExtendVariantWithSlotsProps = {}) => extendVariants(Card, { variants: { + variant: { + flat: "", + filled: "", + test: "", + }, shadow: { none: { base: "shadow-xs", @@ -104,14 +110,33 @@ describe("extendVariants function - no slots", () => { expect(ref.current).not.toBeNull(); }); - test("should render with given text", () => { + test("as Link should work", () => { const Button2 = createExtendNoSlotsComponent(); - render(Press me); + const {container} = render( + + Press me + , + ); + + // Link component from react package - verify it renders + const link = container.querySelector("a"); + expect(link).toBeInTheDocument(); expect(screen.getByText("Press me")).toBeInTheDocument(); }); + test("should render with given text", () => { + const Button2 = createExtendNoSlotsComponent(); + const {container} = render( + Press me, + ); + + const button = container.querySelector("button"); + + expect(button).toHaveTextContent("Press me"); + }); + test("should override the base styles", () => { const Button2 = createExtendNoSlotsComponent(); const {container} = render( @@ -180,6 +205,78 @@ describe("extendVariants function - no slots", () => { expect(button).toHaveClass("scale-150"); }); + + test("as prop should change rendered element to anchor", () => { + const Button2 = createExtendNoSlotsComponent(); + const {container} = render( + + Link Button + , + ); + + const link = container.querySelector("a"); + + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute("href", "/test"); + expect(link).toHaveTextContent("Link Button"); + }); + + test("as prop should change rendered element to div", () => { + const Button2 = createExtendNoSlotsComponent(); + const {container} = render(Div Button); + + const div = container.querySelector("div"); + + expect(div).toBeInTheDocument(); + expect(div).toHaveTextContent("Div Button"); + + // Should not be a button + const button = container.querySelector("button"); + + expect(button).not.toBeInTheDocument(); + }); + + test("ref should work with polymorphic component as anchor", () => { + const ref = React.createRef(); + const Button2 = createExtendNoSlotsComponent(); + + render( + + Link + , + ); + + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + expect(ref.current).toHaveAttribute("href", "/test"); + }); + + test("variant styles should persist with 'as' prop", () => { + const Button2 = createExtendNoSlotsComponent(); + const {container} = render( + + Link + , + ); + + const link = container.querySelector("a"); + + expect(link).toHaveClass("size--2xl"); + // isScalable=true triggers scale-125, but compound variant for size=2xl + isScalable overrides to scale-150 + expect(link).toHaveClass("scale-150"); + }); + + test("compound variant styles should work with 'as' prop", () => { + const Button2 = createExtendNoSlotsComponent(); + const {container} = render( + + Link + , + ); + + const link = container.querySelector("a"); + + expect(link).toHaveClass("scale-150"); // compound variant for size="2xl" + isScalable + }); }); describe("extendVariants function - with slots", () => { diff --git a/packages/core/system-rsc/src/extend-variants.d.ts b/packages/core/system-rsc/src/extend-variants.d.ts index 6a00073603..53c8e4d60f 100644 --- a/packages/core/system-rsc/src/extend-variants.d.ts +++ b/packages/core/system-rsc/src/extend-variants.d.ts @@ -1,6 +1,6 @@ import type {ClassValue, StringToBoolean, OmitUndefined, ClassProp} from "tailwind-variants"; import type {ForwardRefExoticComponent, JSXElementConstructor, RefAttributes} from "react"; -import type * as _heroui_system from "@heroui/system"; +import type {InternalForwardRefRenderFunction} from "./types.js"; type SlotsClassValue = { [K in keyof S]?: ClassValue; @@ -103,7 +103,7 @@ export type ExtendVariants = < slots?: S; }, opts?: Options, -) => _heroui_system.InternalForwardRefRenderFunction< +) => InternalForwardRefRenderFunction< C, { [K in Exclude]?: diff --git a/packages/core/system-rsc/src/extend-variants.js b/packages/core/system-rsc/src/extend-variants.js index 232c05be1f..e35918b2ac 100644 --- a/packages/core/system-rsc/src/extend-variants.js +++ b/packages/core/system-rsc/src/extend-variants.js @@ -124,22 +124,25 @@ export function extendVariants(BaseComponent, styles = {}, opts = {}) { const hasSlots = typeof slots === "object" && Object.keys(slots).length !== 0; const ForwardedComponent = React.forwardRef((originalProps = {}, ref) => { + // Extract 'as' prop if present + const {as: Component = BaseComponent, ...restProps} = originalProps; + const newProps = React.useMemo(() => getClassNamesWithProps( { slots, variants, compoundVariants, - props: originalProps, + props: restProps, // Use restProps without 'as' defaultVariants, hasSlots, opts, }, - [originalProps], + [restProps], ), ); - return React.createElement(BaseComponent, {...originalProps, ...newProps, ref}); + return React.createElement(Component, {...restProps, ...newProps, ref}); }); // Add collection node function for collection-based components diff --git a/packages/core/system-rsc/src/types.ts b/packages/core/system-rsc/src/types.ts index a2534abf1c..1bdafa4779 100644 --- a/packages/core/system-rsc/src/types.ts +++ b/packages/core/system-rsc/src/types.ts @@ -40,7 +40,7 @@ export type MergeWithAs< AsComponent extends As = As, > = (RightJoinProps | RightJoinProps) & { as?: AsComponent; -}; +} & React.RefAttributes; export type InternalForwardRefRenderFunction< Component extends As, From 5eb049e72f3407b7c1f82b8bf67c7f1d950107f6 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 4 Jan 2026 12:55:43 +0800 Subject: [PATCH 17/17] chore(system-rsc): add missing tests --- .../__tests__/extend-variants.test.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/packages/core/system-rsc/__tests__/extend-variants.test.tsx b/packages/core/system-rsc/__tests__/extend-variants.test.tsx index db4fbb1b5d..3d5ce5f41d 100644 --- a/packages/core/system-rsc/__tests__/extend-variants.test.tsx +++ b/packages/core/system-rsc/__tests__/extend-variants.test.tsx @@ -446,4 +446,75 @@ describe("extendVariants function - with slots", () => { expect(baseEl).toHaveClass("rounded-xl"); expect(headerEl).toHaveClass("scale-150"); }); + + test("should override base component slots with direct slots option", () => { + const Card2 = extendVariants(Card, { + slots: { + header: "!font-bold !text-lg", + footer: "!bg-red-500", + }, + }); + + const {getByTestId} = render(Card Content); + + const headerEl = getByTestId("header"); + const footerEl = getByTestId("footer"); + + expect(headerEl).toHaveClass("!font-bold"); + expect(headerEl).toHaveClass("!text-lg"); + expect(footerEl).toHaveClass("!bg-red-500"); + }); + + test("should merge direct slots with variant-based slots", () => { + const Card2 = extendVariants(Card, { + slots: { + header: "!font-bold", + }, + variants: { + shadow: { + xl: { + base: "shadow-xl", + }, + }, + }, + defaultVariants: { + shadow: "xl", + }, + }); + + const {getByTestId} = render(Card Content); + + const baseEl = getByTestId("base"); + const headerEl = getByTestId("header"); + + expect(baseEl).toHaveClass("shadow-xl"); + expect(headerEl).toHaveClass("!font-bold"); + }); + + test("direct slots should override variant-based slots for the same slot", () => { + const Card2 = extendVariants(Card, { + slots: { + base: "!bg-blue-500", + }, + variants: { + shadow: { + xl: { + base: "shadow-xl", + }, + }, + }, + defaultVariants: { + shadow: "xl", + }, + }); + + const {getByTestId} = render(Card Content); + + const baseEl = getByTestId("base"); + + // Direct slots should be applied + expect(baseEl).toHaveClass("!bg-blue-500"); + // Variant-based slots should also be applied (they merge) + expect(baseEl).toHaveClass("shadow-xl"); + }); });