diff --git a/packages/pigment-css-react/package.json b/packages/pigment-css-react/package.json
index 7e7c9c809ce883..4613a04db77e02 100644
--- a/packages/pigment-css-react/package.json
+++ b/packages/pigment-css-react/package.json
@@ -37,6 +37,7 @@
"@babel/parser": "^7.23.9",
"@babel/types": "^7.23.9",
"@emotion/css": "^11.11.2",
+ "@emotion/is-prop-valid": "^1.2.2",
"@emotion/react": "^11.11.4",
"@emotion/serialize": "^1.1.3",
"@emotion/styled": "^11.11.0",
diff --git a/packages/pigment-css-react/src/styled.jsx b/packages/pigment-css-react/src/styled.js
similarity index 66%
rename from packages/pigment-css-react/src/styled.jsx
rename to packages/pigment-css-react/src/styled.js
index 05e0721c79902e..0febf1e7df4512 100644
--- a/packages/pigment-css-react/src/styled.jsx
+++ b/packages/pigment-css-react/src/styled.js
@@ -1,5 +1,6 @@
import * as React from 'react';
import clsx from 'clsx';
+import isPropValid from '@emotion/is-prop-valid';
function getVariantClasses(componentProps, variants) {
const { ownerState = {} } = componentProps;
@@ -15,16 +16,21 @@ function getVariantClasses(componentProps, variants) {
return variantClasses;
}
-/**
- * @param {string} propKey
- * @returns {boolean}
- */
-function defaultShouldForwardProp(propKey) {
- return propKey !== 'sx' && propKey !== 'as' && propKey !== 'ownerState';
+function isHtmlTag(tag) {
+ return (
+ typeof tag === 'string' &&
+ // 96 is one less than the char code
+ // for "a" so this is checking that
+ // it's a lowercase character
+ tag.charCodeAt(0) > 96
+ );
}
+const slotShouldForwardProp = (key) => key !== 'sx' && key !== 'as' && key !== 'ownerState';
+const rootShouldForwardProp = (key) => slotShouldForwardProp(key) && key !== 'classes';
+
/**
- * @typedef {typeof defaultShouldForwardProp} ShouldForwardProp
+ * @typedef {(propKey: string) => boolean} ShouldForwardProp
*/
/**
@@ -40,10 +46,20 @@ function defaultShouldForwardProp(propKey) {
* @param {Object} componentMeta.defaultProps Default props object copied over and inlined from theme object
*/
export default function styled(tag, componentMeta = {}) {
- const { name, slot, shouldForwardProp = defaultShouldForwardProp } = componentMeta;
+ const { name, slot, shouldForwardProp } = componentMeta;
+
+ let finalShouldForwardProp = shouldForwardProp;
+ if (!shouldForwardProp) {
+ if (isHtmlTag(tag)) {
+ finalShouldForwardProp = isPropValid;
+ } else if (slot === 'Root' || slot === 'root') {
+ finalShouldForwardProp = rootShouldForwardProp;
+ } else {
+ finalShouldForwardProp = slotShouldForwardProp;
+ }
+ }
+ const shouldUseAs = !finalShouldForwardProp('as');
/**
- * @TODO - Filter props and only pass necessary props to children
- *
* This is the runtime `styled` function that finally renders the component
* after transpilation through WyW-in-JS. It makes sure to add the base classes,
* variant classes if they satisfy the prop value and also adds dynamic css
@@ -62,26 +78,10 @@ export default function styled(tag, componentMeta = {}) {
*/
function scopedStyledWithOptions(options = {}) {
const { displayName, classes = [], vars: cssVars = {}, variants = [] } = options;
- let componentName = 'Component';
-
- if (name) {
- if (slot) {
- componentName = `${name}${slot}`;
- } else {
- componentName = name;
- }
- } else if (displayName) {
- componentName = displayName;
- }
- const StyledComponent = React.forwardRef(function StyledComponent(
- // eslint-disable-next-line react/prop-types
- { as, className, sx, style, ...props },
- ref,
- ) {
- // eslint-disable-next-line react/prop-types
- const { ownerState, ...restProps } = props;
- const Component = as ?? tag;
+ const StyledComponent = React.forwardRef(function StyledComponent(inProps, ref) {
+ const { as, className, sx, style, ownerState, ...props } = inProps;
+ const Component = (shouldUseAs && as) || tag;
const varStyles = Object.entries(cssVars).reduce(
(acc, [cssVariable, [variableFunction, isUnitLess]]) => {
const value = variableFunction(props);
@@ -97,9 +97,7 @@ export default function styled(tag, componentMeta = {}) {
},
{},
);
- // eslint-disable-next-line react/prop-types
const sxClass = typeof sx === 'string' ? sx : sx?.className;
- // eslint-disable-next-line react/prop-types
const sxVars = sx && typeof sx !== 'string' ? sx.vars : undefined;
if (sxVars) {
@@ -112,40 +110,31 @@ export default function styled(tag, componentMeta = {}) {
});
}
- const finalClassName = clsx(classes, sxClass, className, getVariantClasses(props, variants));
- const toPassProps = Object.keys(restProps)
- .filter((item) => {
- const res = shouldForwardProp(item);
- if (res) {
- return defaultShouldForwardProp(item);
- }
- return false;
- })
- .reduce((acc, key) => {
- acc[key] = restProps[key];
- return acc;
- }, {});
+ const finalClassName = clsx(
+ classes,
+ sxClass,
+ className,
+ getVariantClasses(inProps, variants),
+ );
+
+ const newProps = {};
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in props) {
+ if (shouldUseAs && key === 'as') {
+ continue;
+ }
- // eslint-disable-next-line no-underscore-dangle
- if (!Component.__isStyled || typeof Component === 'string') {
- return (
- // eslint-disable-next-line react/jsx-filename-extension
-
- );
+ if (finalShouldForwardProp(key)) {
+ newProps[key] = props[key];
+ }
}
return (
ownerState.color === 'secondary',
+ style: {
+ color: 'salmon',
+ },
+ },
+ ],
+});
diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.css b/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.css
new file mode 100644
index 00000000000000..4a6e20753801af
--- /dev/null
+++ b/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.css
@@ -0,0 +1,6 @@
+.b1prasel-1 {
+ color: tomato;
+}
+.b1prasel-2 {
+ color: salmon;
+}
diff --git a/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.js b/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.js
new file mode 100644
index 00000000000000..6b450dcd6f112a
--- /dev/null
+++ b/packages/pigment-css-react/tests/styled/fixtures/styled-variants.output.js
@@ -0,0 +1,17 @@
+import { styled as _styled } from '@pigment-css/react';
+import _theme from '@pigment-css/react/theme';
+const Button = /*#__PURE__*/ _styled('button')({
+ classes: ['b1prasel'],
+ variants: [
+ {
+ props: {
+ color: 'primary',
+ },
+ className: 'b1prasel-1',
+ },
+ {
+ props: ({ ownerState }) => ownerState.color === 'secondary',
+ className: 'b1prasel-2',
+ },
+ ],
+});
diff --git a/packages/pigment-css-react/tests/styled/runtime-styled.test.js b/packages/pigment-css-react/tests/styled/runtime-styled.test.js
new file mode 100644
index 00000000000000..d2f325b082b0a3
--- /dev/null
+++ b/packages/pigment-css-react/tests/styled/runtime-styled.test.js
@@ -0,0 +1,205 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer } from '@mui-internal/test-utils';
+import styled from '../../src/styled';
+
+describe('props filtering', () => {
+ const { render } = createRenderer();
+
+ it('composes shouldForwardProp on composed styled components', () => {
+ const StyledDiv = styled('div', {
+ shouldForwardProp: (prop) => prop !== 'foo',
+ })();
+
+ const ComposedDiv = styled(StyledDiv, {
+ shouldForwardProp: (prop) => prop !== 'bar',
+ })();
+
+ const { container } = render();
+
+ expect(container.firstChild).to.not.have.attribute('foo');
+ expect(container.firstChild).to.not.have.attribute('bar');
+ expect(container.firstChild).to.have.attribute('xyz', 'true');
+ });
+
+ it('custom shouldForwardProp works', () => {
+ function Svg(props) {
+ return (
+
+ );
+ }
+
+ const StyledSvg = styled(Svg, {
+ shouldForwardProp: (prop) => ['className', 'width', 'height'].indexOf(prop) !== -1,
+ })`
+ &,
+ & * {
+ fill: ${({ color }) => color};
+ }
+ `;
+
+ const { container } = render();
+ expect(container.firstChild).to.not.have.attribute('color');
+ expect(container.firstChild).to.have.attribute('width', '100px');
+ expect(container.firstChild).to.have.attribute('height', '100px');
+ });
+
+ it('default prop filtering for native html tag', () => {
+ const Link = styled('a')`
+ color: green;
+ `;
+ const rest = { m: [3], pt: [4] };
+
+ const { container } = render(
+
+ hello world
+ ,
+ );
+ expect(container.firstChild).to.have.attribute('href', 'link');
+ expect(container.firstChild).to.have.attribute('aria-label', 'some label');
+ expect(container.firstChild).to.have.attribute('data-wow', 'value');
+ expect(container.firstChild).to.have.attribute('is', 'true');
+
+ expect(container.firstChild).not.to.have.attribute('a');
+ expect(container.firstChild).not.to.have.attribute('b');
+ expect(container.firstChild).not.to.have.attribute('wow');
+ expect(container.firstChild).not.to.have.attribute('prop');
+ expect(container.firstChild).not.to.have.attribute('filtering');
+ expect(container.firstChild).not.to.have.attribute('cool');
+ });
+
+ it('no prop filtering on non string tags', () => {
+ // eslint-disable-next-line jsx-a11y/anchor-has-content
+ const Link = styled((props) => )`
+ color: green;
+ `;
+
+ const { container } = render(
+
+ hello world
+ ,
+ );
+
+ expect(container.firstChild).to.have.attribute('href', 'link');
+ expect(container.firstChild).to.have.attribute('aria-label', 'some label');
+ expect(container.firstChild).to.have.attribute('data-wow', 'value');
+ expect(container.firstChild).to.have.attribute('is', 'true');
+ expect(container.firstChild).to.have.attribute('a', 'true');
+ expect(container.firstChild).to.have.attribute('b', 'true');
+ expect(container.firstChild).to.have.attribute('wow', 'true');
+ expect(container.firstChild).to.have.attribute('prop', 'true');
+ expect(container.firstChild).to.have.attribute('filtering', 'true');
+ expect(container.firstChild).to.have.attribute('cool', 'true');
+ });
+
+ describe('ownerState prop', () => {
+ it('[HTML tag] does not forward `ownerState` by default', () => {
+ const StyledDiv = styled('div')();
+
+ const { container } = render();
+ expect(container.firstChild).not.to.have.attribute('ownerState');
+ });
+
+ it('does not forward `ownerState` to other React component', () => {
+ function InnerComponent(props) {
+ const { ownerState } = props;
+ return ;
+ }
+ const StyledDiv = styled(InnerComponent)();
+
+ const { container } = render();
+ expect(container.firstChild).not.to.have.attribute('ownerState');
+ expect(container.firstChild).to.have.attribute('data-ownerstate', 'false');
+ });
+
+ it('forward `ownerState` to inherited styled component', () => {
+ const StyledDiv = styled('div')({
+ classes: ['div1'],
+ variants: [
+ {
+ props: ({ ownerState }) => ownerState.color === 'secondary',
+ className: 'div1-secondary',
+ },
+ ],
+ });
+
+ const StyledDiv2 = styled(StyledDiv)({
+ classes: ['div2'],
+ variants: [
+ {
+ props: ({ ownerState }) => ownerState.color === 'secondary',
+ className: 'div2-secondary',
+ },
+ ],
+ });
+
+ const { container } = render();
+ expect(container.firstChild).to.have.class('div1-secondary');
+ expect(container.firstChild).to.have.class('div2-secondary');
+ });
+ });
+
+ describe('classes prop', () => {
+ it('does not forward `classes` by default', () => {
+ const StyledDiv = styled('div')();
+
+ const { container } = render();
+ expect(container.firstChild).not.to.have.attribute('classes');
+ });
+
+ it('does not forward `classes` for the root slot to other React component', () => {
+ function InnerComponent(props) {
+ const { classes = {} } = props;
+ return ;
+ }
+ const StyledDiv = styled(InnerComponent, {
+ name: 'Div',
+ slot: 'root',
+ })();
+
+ const { container } = render();
+ expect(container.firstChild).not.to.have.attribute('classes');
+ expect(container.firstChild).not.to.have.class('root-123');
+ });
+
+ it('forward `classes` for the root slot by a custom shouldForwardProp', () => {
+ function ButtonBase(props) {
+ const { classes = {} } = props;
+ return ;
+ }
+ const ButtonRoot = styled(ButtonBase, {
+ name: 'Div',
+ slot: 'root',
+ shouldForwardProp: (prop) => prop === 'classes',
+ })();
+
+ const { container } = render();
+ expect(container.firstChild).to.have.class('root-123');
+ });
+ });
+});
diff --git a/packages/pigment-css-react/tests/styled/styled.test.ts b/packages/pigment-css-react/tests/styled/styled.test.tsx
similarity index 81%
rename from packages/pigment-css-react/tests/styled/styled.test.ts
rename to packages/pigment-css-react/tests/styled/styled.test.tsx
index b061767fc27d10..4e555d7497c670 100644
--- a/packages/pigment-css-react/tests/styled/styled.test.ts
+++ b/packages/pigment-css-react/tests/styled/styled.test.tsx
@@ -67,4 +67,18 @@ describe('Pigment CSS - styled', () => {
expect(output.js).to.equal(fixture.js);
expect(output.css).to.equal(fixture.css);
});
+
+ it('should work with variants', async () => {
+ const { output, fixture } = await runTransformation(
+ path.join(__dirname, 'fixtures/styled-variants.input.js'),
+ {
+ themeArgs: {
+ theme,
+ },
+ },
+ );
+
+ expect(output.js).to.equal(fixture.js);
+ expect(output.css).to.equal(fixture.css);
+ });
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7b3f66cce2438d..ccdc319d95bb87 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2166,6 +2166,9 @@ importers:
'@emotion/css':
specifier: ^11.11.2
version: 11.11.2
+ '@emotion/is-prop-valid':
+ specifier: ^1.2.2
+ version: 1.2.2
'@emotion/react':
specifier: ^11.11.4
version: 11.11.4(@types/react@18.2.55)(react@18.2.0)
@@ -4429,6 +4432,11 @@ packages:
dependencies:
'@emotion/memoize': 0.8.1
+ /@emotion/is-prop-valid@1.2.2:
+ resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==}
+ dependencies:
+ '@emotion/memoize': 0.8.1
+
/@emotion/memoize@0.7.1:
resolution: {integrity: sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==}
dev: false
@@ -4501,7 +4509,7 @@ packages:
dependencies:
'@babel/runtime': 7.24.1
'@emotion/babel-plugin': 11.11.0
- '@emotion/is-prop-valid': 1.2.1
+ '@emotion/is-prop-valid': 1.2.2
'@emotion/react': 11.11.4(@types/react@18.2.55)(react@18.2.0)
'@emotion/serialize': 1.1.3
'@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
@@ -22458,6 +22466,7 @@ packages:
'@babel/parser': 7.24.1
'@babel/types': 7.24.0
'@emotion/css': 11.11.2
+ '@emotion/is-prop-valid': 1.2.2
'@emotion/react': 11.11.4(@types/react@18.2.55)(react@18.2.0)
'@emotion/serialize': 1.1.3
'@emotion/styled': 11.11.0(@emotion/react@11.11.4)(@types/react@18.2.55)(react@18.2.0)
diff --git a/tsup.config.ts b/tsup.config.ts
index 159ef80835ef07..255be9ede538e7 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -30,4 +30,7 @@ export default defineConfig({
env: {
PACKAGE_NAME: pkgJson.name,
},
+ loader: {
+ '.js': 'jsx',
+ },
});