Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for slot level variant overrides #82

Merged
merged 3 commits into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"esm"
]
},
"packageManager": "pnpm@8.3.1",
mskelton marked this conversation as resolved.
Show resolved Hide resolved
"packageManager": "pnpm@8.6.8",
"engines": {
"node": ">=16.x",
"pnpm": ">=7.x"
Expand Down
113 changes: 113 additions & 0 deletions src/__tests__/tv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,119 @@ describe("Tailwind Variants (TV) - Slots", () => {
expectTv(list(), ["list-none", "color--secondary-list", "compound--list"]);
expectTv(wrapper(), ["flex", "flex-col", "color--secondary-wrapper", "compound--wrapper"]);
});

test("should support slot level variant overrides", () => {
const menu = tv({
base: "text-3xl",
slots: {
title: "text-2xl",
},
variants: {
color: {
primary: {
base: "color--primary-base",
title: "color--primary-title",
},
secondary: {
base: "color--secondary-base",
title: "color--secondary-title",
},
},
},
defaultVariants: {
color: "primary",
},
});

const {base, title} = menu();

expectTv(base(), ["text-3xl", "color--primary-base"]);
expectTv(title(), ["text-2xl", "color--primary-title"]);
expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]);
expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title"]);
});

test("should support slot level variant overrides - compoundSlots", () => {
const menu = tv({
base: "text-3xl",
slots: {
title: "text-2xl",
subtitle: "text-xl",
},
variants: {
color: {
primary: {
base: "color--primary-base",
title: "color--primary-title",
subtitle: "color--primary-subtitle",
},
secondary: {
base: "color--secondary-base",
title: "color--secondary-title",
subtitle: "color--secondary-subtitle",
},
},
},
compoundSlots: [
{
slots: ["title", "subtitle"],
color: "secondary",
class: ["truncate"],
},
],
defaultVariants: {
color: "primary",
},
});

const {base, title, subtitle} = menu();

expectTv(base(), ["text-3xl", "color--primary-base"]);
expectTv(title(), ["text-2xl", "color--primary-title"]);
expectTv(subtitle(), ["text-xl", "color--primary-subtitle"]);
expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]);
expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title", "truncate"]);
expectTv(subtitle({color: "secondary"}), ["text-xl", "color--secondary-subtitle", "truncate"]);
});

test("should support slot level variant overrides - compoundVariants", () => {
const menu = tv({
base: "text-3xl",
slots: {
title: "text-2xl",
},
variants: {
color: {
primary: {
base: "color--primary-base",
title: "color--primary-title",
},
secondary: {
base: "color--secondary-base",
title: "color--secondary-title",
},
},
},
compoundVariants: [
{
color: "secondary",
class: {
title: "truncate",
},
},
],
defaultVariants: {
color: "primary",
},
});

const {base, title} = menu();

expectTv(base(), ["text-3xl", "color--primary-base"]);
expectTv(title(), ["text-2xl", "color--primary-title"]);
expectTv(base({color: "secondary"}), ["text-3xl", "color--secondary-base"]);
expectTv(title({color: "secondary"}), ["text-2xl", "color--secondary-title", "truncate"]);
});
});

describe("Tailwind Variants (TV) - Compound Slots", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ export type TVReturnType<
(props?: TVProps<V, S, C, EV, ES>): ES extends undefined
? S extends undefined
? string
: {[K in TVSlotsWithBase<S, B>]: (slotProps?: ClassProp) => string}
: {[K in TVSlotsWithBase<ES & S, B>]: (slotProps?: ClassProp) => string};
: {[K in TVSlotsWithBase<S, B>]: (slotProps?: TVProps<V, S, C, EV, ES>) => string}
: {[K in TVSlotsWithBase<ES & S, B>]: (slotProps?: TVProps<V, S, C, EV, ES>) => string};
} & TVReturnProps<V, S, B, EV, ES, E>;

export type TV = {
Expand Down
38 changes: 18 additions & 20 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,14 @@ export const tv = (options, configProp) => {
return result;
};

const getVariantValue = (variant, vrs = variants, slotKey = null) => {
const getVariantValue = (variant, vrs = variants, slotKey = null, slotProps = null) => {
const variantObj = vrs?.[variant];

if (!variantObj || isEmptyObject(variantObj)) {
return null;
}

const variantProp = props?.[variant];
const variantProp = slotProps?.[variant] ?? props?.[variant];

if (variantProp === null) return null;

Expand Down Expand Up @@ -207,13 +207,13 @@ export const tv = (options, configProp) => {
return Object.keys(variants).map((vk) => getVariantValue(vk, variants));
};

const getVariantClassNamesBySlotKey = (slotKey) => {
const getVariantClassNamesBySlotKey = (slotKey, slotProps) => {
if (!variants || typeof variants !== "object") {
return null;
}

return Object.keys(variants).reduce((acc, variant) => {
const variantValue = getVariantValue(variant, variants, slotKey);
const variantValue = getVariantValue(variant, variants, slotKey, slotProps);

const value =
slotKey === "base" && typeof variantValue === "string"
Expand All @@ -231,7 +231,7 @@ export const tv = (options, configProp) => {
const propsWithoutUndefined =
props && Object.fromEntries(Object.entries(props).filter(([, value]) => value !== undefined));

const getCompleteProps = (key) => {
const getCompleteProps = (key, slotProps) => {
const initialProp =
typeof props?.[key] === "object"
? {
Expand All @@ -243,15 +243,16 @@ export const tv = (options, configProp) => {
...defaultVariants,
...propsWithoutUndefined,
...initialProp,
...slotProps,
};
};

const getCompoundVariantsValue = (cv = []) =>
const getCompoundVariantsValue = (cv = [], slotProps) =>
cv
// eslint-disable-next-line @typescript-eslint/no-unused-vars
?.filter(({class: tvClass, className: tvClassName, ...compoundVariantOptions}) =>
Object.entries(compoundVariantOptions).every(([key, value]) => {
const completeProps = getCompleteProps(key);
const completeProps = getCompleteProps(key, slotProps);

return Array.isArray(value)
? value.includes(completeProps[key])
Expand All @@ -260,15 +261,15 @@ export const tv = (options, configProp) => {
)
.flatMap(({class: tvClass, className: tvClassName}) => [tvClass, tvClassName]);

const getCompoundVariantClassNames = () => {
const cvValues = getCompoundVariantsValue(compoundVariants);
const ecvValues = getCompoundVariantsValue(extend?.compoundVariants);
const getCompoundVariantClassNames = (slotProps) => {
const cvValues = getCompoundVariantsValue(compoundVariants, slotProps);
const ecvValues = getCompoundVariantsValue(extend?.compoundVariants, slotProps);

return flatMergeArrays(ecvValues, cvValues);
};

const getCompoundVariantClassNamesBySlot = () => {
const compoundClassNames = getCompoundVariantClassNames(compoundVariants);
const getCompoundVariantClassNamesBySlot = (slotProps) => {
const compoundClassNames = getCompoundVariantClassNames(slotProps);

if (!Array.isArray(compoundClassNames)) {
return compoundClassNames;
Expand All @@ -291,7 +292,7 @@ export const tv = (options, configProp) => {
}, {});
};

const getCompoundSlotClassNameBySlot = () => {
const getCompoundSlotClassNameBySlot = (slotProps) => {
if (compoundSlots.length < 1) {
return null;
}
Expand All @@ -303,7 +304,7 @@ export const tv = (options, configProp) => {
const slotVariantsKeys = Object.keys(slotVariants);

for (const key of slotVariantsKeys) {
const completePropsValue = getCompleteProps(key)[key];
const completePropsValue = getCompleteProps(key, slotProps)[key];

// if none of the slot variant keys are included in props or default variants then skip the slot
// if the included slot variant key is not equal to the slot variant value then skip the slot
Expand All @@ -327,18 +328,15 @@ export const tv = (options, configProp) => {

// with slots
if (!isEmptyObject(slotProps) || !isEmptyObject(extend?.slots)) {
const compoundClassNames = getCompoundVariantClassNamesBySlot() ?? [];
const compoundSlotClassNames = getCompoundSlotClassNameBySlot() ?? [];

const slotsFns =
typeof slots === "object" && !isEmptyObject(slots)
? Object.keys(slots).reduce((acc, slotKey) => {
acc[slotKey] = (slotProps) =>
cn(
slots[slotKey],
getVariantClassNamesBySlotKey(slotKey),
compoundClassNames?.[slotKey],
compoundSlotClassNames?.[slotKey],
getVariantClassNamesBySlotKey(slotKey, slotProps),
(getCompoundVariantClassNamesBySlot(slotProps) ?? [])[slotKey],
(getCompoundSlotClassNameBySlot(slotProps) ?? [])[slotKey],
slotProps?.class,
slotProps?.className,
)(config);
Expand Down