diff --git a/modules/styling/lib/cs.ts b/modules/styling/lib/cs.ts index 5867d01dd6..b5747ef7fd 100644 --- a/modules/styling/lib/cs.ts +++ b/modules/styling/lib/cs.ts @@ -753,8 +753,13 @@ export type StencilCompoundConfig = { styles: SerializedStyles | CSSObjectWithVars; }; -type ModifierValuesStencil = { - [P in keyof M]?: MaybeBoolean; +type ModifierValuesStencil< + M extends StencilModifierConfig, + V extends Record | Record> = {} +> = { + [P in keyof M]?: P extends keyof V + ? MaybeBoolean | (string & {}) // If both modifiers and variables define the same key, the value can be either a modifier or a string + : MaybeBoolean; }; export interface StencilConfig< @@ -852,7 +857,7 @@ export type Stencil< V extends Record | Record>, ID extends string = never > = { - (modifiers?: ModifierValuesStencil & VariableValuesStencil): { + (modifiers?: ModifierValuesStencil & VariableValuesStencil): { className: string; style?: Record; }; diff --git a/modules/styling/spec/cs.spec.tsx b/modules/styling/spec/cs.spec.tsx index e2f65b52ea..9b5ff633a3 100644 --- a/modules/styling/spec/cs.spec.tsx +++ b/modules/styling/spec/cs.spec.tsx @@ -760,6 +760,46 @@ describe('createStyles', () => { } expect(found).toEqual(true); }); + + it('should handle both variables and modifiers sharing the same key', () => { + const myStencil = createStencil({ + vars: { + width: '10px', + height: '10px', + }, + base({width}) { + return {width: width}; + }, + modifiers: { + width: { + zero: { + width: '0', + }, + }, + foo: { + true: {}, + }, + }, + }); + + type Arg = Parameters[0]; + expectTypeOf().toHaveProperty('width'); + expectTypeOf().toMatchTypeOf<(string & {}) | 'zero' | undefined>(); + + const result = myStencil({width: '70px', height: '10px'}); + expect(result).toHaveProperty('style'); + expect(result.style).toHaveProperty(myStencil.vars.width, '70px'); + + // only match the base + expect(result.className).toEqual(myStencil.base); + + const result2 = myStencil({width: 'zero', height: '10px'}); + expect(result2).toHaveProperty('style'); + expect(result2.style).toHaveProperty(myStencil.vars.width, 'zero'); + + // match base and width modifier + expect(result2.className).toEqual(`${myStencil.base} ${myStencil.modifiers.width.zero}`); + }); }); }); diff --git a/modules/styling/stories/Basics.stories.mdx b/modules/styling/stories/Basics.stories.mdx index 58c8984731..ae781b7cc7 100644 --- a/modules/styling/stories/Basics.stories.mdx +++ b/modules/styling/stories/Basics.stories.mdx @@ -313,6 +313,40 @@ Notice the stencil adds all the class names that match the base, modifiers, and +### Variables and Modifiers with same keys + +It is possible to have a variable and modifier sharing the same key. The Stencil will accept either +the modifier option or a string. The value will be sent as a variable regardless while the modifer +will only match if it is a valid modifer key. + +```tsx +const myStencil = createStencil({ + vars: { + width: '10px', + }, + base({width}): { + return { + width: width + } + }, + modifiers: { + width: { + zero: { + width: '0', // overrides base styles + }, + }, + }, +}) + +// `'zero'` is part of autocomplete +myStencil({width: 'zero'}); +// returns {className: 'css-base css--width-zero', styles: { '--width': 'zero'}} + +// width also accepts a string +myStencil({width: '10px'}); +// returns {className: 'css-base', styles: { '--width': '10px'}} +``` + ### `keyframes` The `keyframes` function re-exports the [Emotion CSS keyframes](https://emotion.sh/docs/keyframes)