From 31b803358eb2d29e770ba6ef799c857486ddfc50 Mon Sep 17 00:00:00 2001 From: Valentin Politov Date: Fri, 27 Sep 2024 02:06:03 +0300 Subject: [PATCH] [Slot] Deduplicate merged `className` prop (#2336) Co-authored-by: Chance Strickland --- packages/react/slot/src/Slot.test.tsx | 37 +++++++++++++++++++++++++++ packages/react/slot/src/Slot.tsx | 21 +++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/react/slot/src/Slot.test.tsx b/packages/react/slot/src/Slot.test.tsx index c7912b46fe..946787d8e5 100644 --- a/packages/react/slot/src/Slot.test.tsx +++ b/packages/react/slot/src/Slot.test.tsx @@ -106,6 +106,43 @@ describe('given a slotted Trigger', () => { expect(handleChildClick).toHaveBeenCalledTimes(1); }); }); + + describe('with className on itself', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('should apply the className to the child', () => { + expect(screen.getByRole('button')).toHaveClass('btn'); + }); + }); + + describe('with className on itself AND the child', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('should merge className and apply it to the child', () => { + expect(screen.getByRole('button')).toHaveClass('btn', 'btn-sm', 'btn-primary'); + }); + + it('should deduplicate merged className', () => { + const classNames = screen.getByRole('button').className.split(' '); + + expect(classNames).toHaveLength(3); + expect(classNames.filter((name) => name === 'btn')).toHaveLength(1); + }); + }); }); describe('given a Button with Slottable', () => { diff --git a/packages/react/slot/src/Slot.tsx b/packages/react/slot/src/Slot.tsx index c222b1d7f0..655af25caa 100644 --- a/packages/react/slot/src/Slot.tsx +++ b/packages/react/slot/src/Slot.tsx @@ -115,8 +115,25 @@ function mergeProps(slotProps: AnyProps, childProps: AnyProps) { // if it's `style`, we merge them else if (propName === 'style') { overrideProps[propName] = { ...slotPropValue, ...childPropValue }; - } else if (propName === 'className') { - overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' '); + } + // if it's `className`, we deduplicate and merge them + else if (propName === 'className') { + const classNameSet = new Set(); + + if (slotPropValue && typeof slotPropValue === 'string') { + slotPropValue + .split(' ') + .filter(Boolean) + .forEach((name) => classNameSet.add(name)); + } + if (childPropValue && typeof childPropValue === 'string') { + childPropValue + .split(' ') + .filter(Boolean) + .forEach((name) => classNameSet.add(name)); + } + + overrideProps[propName] = [...classNameSet].join(' '); } }