diff --git a/src/makeStyles.tsx b/src/makeStyles.tsx index ddb6b46..fc05d6d 100644 --- a/src/makeStyles.tsx +++ b/src/makeStyles.tsx @@ -2,12 +2,13 @@ import { useMemo } from "react"; import { objectFromEntries } from "./tools/polyfills/Object.fromEntries"; import { objectKeys } from "./tools/objectKeys"; -import type { CSSObject } from "./types"; +import type { CSSObject, CSSInterpolation } from "./types"; import { useCssAndCx } from "./cssAndCx"; import { getDependencyArrayRef } from "./tools/getDependencyArrayRef"; import { typeGuard } from "./tools/typeGuard"; import { useTssEmotionCache } from "./cache"; import { assert } from "./tools/assert"; +import { mergeClasses } from "./mergeClasses"; const getCounter = (() => { let counter = 0; @@ -53,14 +54,23 @@ export function createMakeStyles(params: { useTheme: () => Theme }) { const outerCounter = getCounter(); - return function useStyles(params: Params) { + return function useStyles( + params: Params, + styleOverrides?: { + props: { classes?: Record } & Record< + string, + unknown + >; + ownerState?: Record; + }, + ) { const theme = useTheme(); const { css, cx } = useCssAndCx(); const cache = useTssEmotionCache(); - return useMemo(() => { + let classes = useMemo(() => { const refClassesCache: Record = {}; type RefClasses = Record< @@ -123,13 +133,93 @@ export function createMakeStyles(params: { useTheme: () => Theme }) { refClassesCache[ruleName]; }); - return { - classes, - theme, - css, - cx, - }; + return classes; }, [cache, css, cx, theme, getDependencyArrayRef(params)]); + + const propsClasses = styleOverrides?.props.classes; + { + classes = useMemo( + () => mergeClasses(classes, propsClasses, cx), + [classes, getDependencyArrayRef(propsClasses), cx], + ); + } + + { + let cssObjectByRuleNameOrGetCssObjectByRuleName: + | Record< + string, + | CSSInterpolation + | ((params: { + ownerState: any; + theme: Theme; + }) => CSSInterpolation) + > + | undefined = undefined; + + try { + cssObjectByRuleNameOrGetCssObjectByRuleName = + name !== undefined + ? (theme as any).components?.[name] + ?.styleOverrides + : undefined; + + // eslint-disable-next-line no-empty + } catch {} + + const themeClasses = useMemo(() => { + if (!cssObjectByRuleNameOrGetCssObjectByRuleName) { + return undefined; + } + + const themeClasses: Record = {}; + + for (const ruleName in cssObjectByRuleNameOrGetCssObjectByRuleName) { + const cssObjectOrGetCssObject = + cssObjectByRuleNameOrGetCssObjectByRuleName[ + ruleName + ]; + + if (!(cssObjectOrGetCssObject instanceof Object)) { + continue; + } + + themeClasses[ruleName] = css( + typeof cssObjectOrGetCssObject === "function" + ? cssObjectOrGetCssObject({ + theme, + "ownerState": + styleOverrides?.ownerState, + ...styleOverrides?.props, + }) + : cssObjectOrGetCssObject, + ); + } + + return themeClasses; + }, [ + cssObjectByRuleNameOrGetCssObjectByRuleName === + undefined + ? undefined + : JSON.stringify( + cssObjectByRuleNameOrGetCssObjectByRuleName, + ), + getDependencyArrayRef(styleOverrides?.props), + getDependencyArrayRef(styleOverrides?.ownerState), + css, + ]); + + classes = useMemo( + () => mergeClasses(classes, themeClasses, cx), + [classes, themeClasses, cx], + ); + } + + return { + classes, + theme, + css, + cx, + }; }; }; } diff --git a/src/mergeClasses.ts b/src/mergeClasses.ts index 80752bc..163539b 100644 --- a/src/mergeClasses.ts +++ b/src/mergeClasses.ts @@ -3,6 +3,7 @@ import type { Cx } from "./types"; import { objectKeys } from "./tools/objectKeys"; +import { getDependencyArrayRef } from "./tools/getDependencyArrayRef"; import { useCssAndCx } from "./cssAndCx"; import { useMemo } from "react"; @@ -12,9 +13,8 @@ export function mergeClasses( cx: Cx, ): Record & (string extends U ? {} : Partial, string>>) { - //NOTE: We use !(not) to be resilient for when it is used in withStyle - //where classes fromFromProps could diverge from the canonical type... - if (!classesFromProps) { + //NOTE: We use this test to be resilient in case classesFromProps is not of the expected type. + if (!(classesFromProps instanceof Object)) { return classesFromUseStyles as any; } @@ -54,6 +54,6 @@ export function useMergedClasses( return useMemo( () => mergeClasses(classes, classesOv, cx), - [classes, classesOv, cx], + [classes, getDependencyArrayRef(classesOv), cx], ); } diff --git a/src/test/apps/spa/src/App.tsx b/src/test/apps/spa/src/App.tsx index 1e4e402..fc6527c 100644 --- a/src/test/apps/spa/src/App.tsx +++ b/src/test/apps/spa/src/App.tsx @@ -1,5 +1,5 @@ -import { memo } from "react"; +import { useReducer, memo } from "react"; import { makeStyles, withStyles } from "makeStyles"; import { GlobalStyles, useMergedClasses } from "tss-react"; import { styled } from "@mui/material"; @@ -158,6 +158,16 @@ export function App(props: { className?: string; }) { + ); @@ -456,3 +466,52 @@ const { TestPr54 } = (() => { return { TestPr54 }; })(); + +const { TestingStyleOverrides } = (() => { + + type Props = { + className?: string; + classes?: Partial["classes"]>; + lightBulbBorderColor: string; + } + + function TestingStyleOverrides(props: Props) { + + const { className } = props; + + const [isOn, toggleIsOn] = useReducer(isOn => !isOn, false); + + const { classes, cx } = useStyles(undefined, { props, "ownerState": { isOn } }); + + return ( +
+
+ +

Div should have a border, background should be white

+

Light bulb should have black border, it should be yellow when turned on.

+
+ ); + + } + + const useStyles = makeStyles({ "name": { TestingStyleOverrides } })(theme => ({ + "root": { + "border": "1px solid black", + "width": 500, + "height": 200, + "position": "relative", + "color": "black" + }, + "lightBulb": { + "position": "absolute", + "width": 50, + "height": 50, + "top": 120, + "left": 500 / 2 - 50, + "borderRadius": "50%" + } + })); + + return { TestingStyleOverrides }; + +})(); \ No newline at end of file diff --git a/src/test/apps/spa/src/index.tsx b/src/test/apps/spa/src/index.tsx index c5fa193..c6e42c7 100644 --- a/src/test/apps/spa/src/index.tsx +++ b/src/test/apps/spa/src/index.tsx @@ -20,12 +20,27 @@ const theme = createTheme({ "palette": { "primary": { "main": "#32CD32" //Limegreen + }, + "info": { + "main": "#ffff00" //Yellow } }, "typography": { "subtitle2": { "fontStyle": "italic" } + }, + "components": { + //@ts-ignore + "TestingStyleOverrides": { + "styleOverrides": { + "lightBulb": ({ theme, ownerState: { isOn }, lightBulbBorderColor }: any) => ({ + "border": `1px solid ${lightBulbBorderColor}`, + "backgroundColor": isOn ? theme.palette.info.main : "grey" + }) + } + + } } }); diff --git a/src/test/apps/ssr/pages/_app.tsx b/src/test/apps/ssr/pages/_app.tsx index c407d3d..24fd76a 100644 --- a/src/test/apps/ssr/pages/_app.tsx +++ b/src/test/apps/ssr/pages/_app.tsx @@ -24,12 +24,27 @@ export function App({ Component, pageProps }: AppProps) { "mode": darkModeActive ? "dark" : "light", "primary": { "main": "#32CD32" //Limegreen + }, + "info": { + "main": "#ffff00" //Yellow } }, "typography": { "subtitle2": { "fontStyle": "italic" } + }, + "components": { + //@ts-ignore + "TestingStyleOverrides": { + "styleOverrides": { + "lightBulb": ({ theme, ownerState: { isOn }, lightBulbBorderColor }: any)=>({ + "border": `1px solid ${lightBulbBorderColor}`, + "backgroundColor": isOn ? theme.palette.info.main : "grey" + }) + } + + } } }), [darkModeActive] diff --git a/src/test/apps/ssr/pages/index.tsx b/src/test/apps/ssr/pages/index.tsx index abf2594..81896a4 100644 --- a/src/test/apps/ssr/pages/index.tsx +++ b/src/test/apps/ssr/pages/index.tsx @@ -1,5 +1,5 @@ -import Head from "next/head"; -import { memo } from "react"; + +import { useReducer, memo } from "react"; import { GlobalStyles, useMergedClasses } from "tss-react"; import { makeStyles, useStyles, withStyles } from "../shared/makeStyles"; import { styled } from "@mui/material"; @@ -199,6 +199,16 @@ const { App } = (() => { + ); @@ -510,3 +520,53 @@ const { TestPr54 } = (() => { return { TestPr54 }; })(); + + +const { TestingStyleOverrides } = (() => { + + type Props = { + className?: string; + classes?: Partial["classes"]>; + lightBulbBorderColor: string; + } + + function TestingStyleOverrides(props: Props) { + + const { className } = props; + + const [isOn, toggleIsOn] = useReducer(isOn => !isOn, false); + + const { classes, cx } = useStyles(undefined, { props, "ownerState": { isOn } }); + + return ( +
+
+ +

Div should have a border, background should be white

+

Light bulb should have black border, it should be yellow when turned on.

+
+ ); + + } + + const useStyles = makeStyles({ "name": { TestingStyleOverrides } })(theme => ({ + "root": { + "border": "1px solid black", + "width": 500, + "height": 200, + "position": "relative", + "color": "black" + }, + "lightBulb": { + "position": "absolute", + "width": 50, + "height": 50, + "top": 120, + "left": 500/2 - 50, + "borderRadius": "50%" + } + })); + + return { TestingStyleOverrides }; + +})(); diff --git a/src/withStyles.tsx b/src/withStyles.tsx index 3743e66..6bbc541 100644 --- a/src/withStyles.tsx +++ b/src/withStyles.tsx @@ -5,7 +5,6 @@ import type { ReactComponent } from "./tools/ReactComponent"; import type { CSSObject } from "./types"; import { createMakeStyles } from "./makeStyles"; import { capitalize } from "./tools/capitalize"; -import { useMergedClasses } from "./mergeClasses"; export function createWithStyles(params: { useTheme: () => Theme }) { const { useTheme } = params; @@ -92,12 +91,10 @@ export function createWithStyles(params: { useTheme: () => Theme }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const Out = forwardRef(function (props, ref) { - const { className, classes: classesFromProps, ...rest } = props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { className, classes: _classes, ...rest } = props; - // eslint-disable-next-line prefer-const - let { classes, cx } = useStyles(props); - - classes = useMergedClasses(classes, classesFromProps || undefined); + const { classes, cx } = useStyles(props, { props }); return ( (params: { useTheme: () => Theme }) { const { useTheme } = params; @@ -79,12 +78,10 @@ export function createWithStyles(params: { useTheme: () => Theme }) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const Out = forwardRef(function (props, ref) { - const { className, classes: classesFromProps, ...rest } = props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { className, classes: _classes, ...rest } = props; - // eslint-disable-next-line prefer-const - let { classes, cx } = useStyles(props); - - classes = useMergedClasses(classes, classesFromProps || undefined); + const { classes, cx } = useStyles(props, { props }); return (