diff --git a/src/StyleContext.tsx b/src/StyleContext.tsx index 923155b..7199d18 100644 --- a/src/StyleContext.tsx +++ b/src/StyleContext.tsx @@ -52,7 +52,13 @@ export function createCache() { export type HashPriority = 'low' | 'high'; -export interface StyleContextProps { +export type LayerComposer = (dependency: ReadonlySet) => string; +export type LayerConfig = { + /** Define the hierarchical order here */ + composer: LayerComposer; +}; + +export interface StyleContextValue { autoClear?: boolean; /** @private Test only. Not work in production. */ mock?: 'server' | 'client'; @@ -77,38 +83,63 @@ export interface StyleContextProps { * Please note that `linters` do not support dynamic update. */ linters?: Linter[]; - /** Wrap css in a layer to avoid global style conflict */ - layer?: boolean; + /** + * Wrap css in a layer to avoid global style conflict + * @see [MDN-CSS Layer](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) + */ + layer?: LayerConfig; } -const StyleContext = React.createContext({ +export const defaultLayerComposer: LayerComposer = (dependency) => + Array.from(dependency).join(); + +const noop = () => ``; + +const StyleContext = React.createContext({ hashPriority: 'low', cache: createCache(), defaultCache: true, }); -export type StyleProviderProps = Partial & { - children?: React.ReactNode; -}; +export interface StyleProviderProps + extends Omit, 'layer'> { + layer?: boolean | LayerConfig; +} -export const StyleProvider: React.FC = (props) => { +export const StyleProvider = ( + props: React.PropsWithChildren, +) => { const { children, ...restProps } = props; const parentContext = React.useContext(StyleContext); - const context = useMemo( + const context = useMemo( () => { - const mergedContext: StyleContextProps = { + const mergedContext: StyleContextValue = { ...parentContext, }; - (Object.keys(restProps) as (keyof StyleContextProps)[]).forEach((key) => { + (Object.keys(restProps) as (keyof StyleContextValue)[]).forEach((key) => { const value = restProps[key]; if (restProps[key] !== undefined) { (mergedContext as any)[key] = value; } }); + // Standardize layer + const { layer } = mergedContext; + if (typeof layer === 'boolean') { + mergedContext.layer = layer + ? { composer: defaultLayerComposer } + : { composer: noop }; + } else if (typeof layer === 'object') { + mergedContext.layer = { + ...layer, + // Ensure composer is always a function + composer: layer.composer ?? defaultLayerComposer, + }; + } + const { cache } = restProps; mergedContext.cache = mergedContext.cache || createCache(); mergedContext.defaultCache = !cache && parentContext.defaultCache; diff --git a/src/hooks/useStyleRegister.tsx b/src/hooks/useStyleRegister.tsx index a0b7e7f..70d7783 100644 --- a/src/hooks/useStyleRegister.tsx +++ b/src/hooks/useStyleRegister.tsx @@ -9,7 +9,7 @@ import type { Theme, Transformer } from '..'; import type Keyframes from '../Keyframes'; import type { Linter } from '../linters'; import { contentQuotesLinter, hashedAnimationLinter } from '../linters'; -import type { HashPriority } from '../StyleContext'; +import type { HashPriority, LayerComposer } from '../StyleContext'; import StyleContext, { ATTR_CACHE_PATH, ATTR_MARK, @@ -128,7 +128,9 @@ function injectSelectorHash( export interface ParseConfig { hashId?: string; hashPriority?: HashPriority; - layer?: LayerConfig; + layer?: LayerConfig & { + composer?: LayerComposer; + }; path?: string; transformers?: Transformer[]; linters?: Linter[]; @@ -333,15 +335,17 @@ export const parseStyle = ( if (!root) { styleStr = `{${styleStr}}`; } else if (layer) { + const { name, dependencies, composer } = layer; // fixme: https://github.com/thysultan/stylis/pull/339 if (styleStr) { - styleStr = `@layer ${layer.name} {${styleStr}}`; + styleStr = `@layer ${name} {${styleStr}}`; } - if (layer.dependencies) { - effectStyle[`@layer ${layer.name}`] = layer.dependencies - .map((deps) => `@layer ${deps}, ${layer.name};`) - .join('\n'); + if (dependencies) { + const dependency = new Set([...dependencies, name]); + const combinedDependencies = + composer?.(dependency) ?? Array.from(dependency).join(', '); + effectStyle[`@layer ${name}`] = `@layer ${combinedDependencies};`; } } @@ -402,9 +406,10 @@ export default function useStyleRegister( transformers, linters, cache, - layer: enableLayer, + layer: ctxLayer, } = React.useContext(StyleContext); const tokenKey = token._tokenKey as string; + const enableLayer = 'composer' in (ctxLayer || {}); const fullPath = [tokenKey]; if (enableLayer) { @@ -446,7 +451,12 @@ export default function useStyleRegister( const [parsedStyle, effectStyle] = parseStyle(styleObj, { hashId, hashPriority, - layer: enableLayer ? layer : undefined, + layer: enableLayer + ? { + ...layer!, + composer: ctxLayer!.composer, + } + : undefined, path: path.join('-'), transformers, linters, diff --git a/tests/layer.spec.tsx b/tests/layer.spec.tsx index 2c1850a..4a8cd21 100644 --- a/tests/layer.spec.tsx +++ b/tests/layer.spec.tsx @@ -96,4 +96,45 @@ describe('layer', () => { const styles = Array.from(document.head.querySelectorAll('style')); expect(styles[0].innerHTML.trim()).toEqual(''); }); + + // https://github.com/ant-design/pro-components/issues/8955 + it('custom layer composer', () => { + const theme = createTheme(() => ({})); + const Demo = () => { + useStyleRegister( + { + theme, + token: { _tokenKey: 'test' }, + path: ['shared'], + layer: { + name: 'pro', + dependencies: ['basic'], + }, + }, + () => ({ + p: { + color: 'red', + }, + }), + ); + return null; + }; + + render( + + ['tw-base', ...Array.from(deps), 'tw-utils'].join(', '), + }} + cache={createCache()} + > + + , + ); + + const styles = Array.from(document.head.querySelectorAll('style')); + expect(styles[0].innerHTML.trim()).toEqual( + '@layer tw-base,basic,pro,tw-utils;', + ); + }); });