Skip to content

Commit

Permalink
fix(stencil): TypeScript support for variable/modifier with same key (#…
Browse files Browse the repository at this point in the history
…2635)

Fix TypeScript types when the variable and modifiers share the same keys. This is supported without issue at runtime, but TypeScript would throw an error. Now if the keys are the same, the types will include the modifier keys, but allow any string.

For example, the following is now supported:
```tsx
const myStencil = createStencil({
  vars: {
    width: '10px',
  },
  base({width}): {
    return {
      width: width
    }
  },
  modifiers: {
    width: {
      zero: {
        width: '0', // overrides base styles
      },
    },
  },
});

myStencil({width: 'zero'}); // `'zero'` is part of autocomplete
myStencil({width: '10px'}); // width also accepts a string
```

[category:Styling]
  • Loading branch information
NicholasBoll committed Mar 11, 2024
1 parent c604a4e commit b2f8a99
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 3 deletions.
11 changes: 8 additions & 3 deletions modules/styling/lib/cs.ts
Expand Up @@ -753,8 +753,13 @@ export type StencilCompoundConfig<M> = {
styles: SerializedStyles | CSSObjectWithVars;
};

type ModifierValuesStencil<M extends StencilModifierConfig> = {
[P in keyof M]?: MaybeBoolean<keyof M[P]>;
type ModifierValuesStencil<
M extends StencilModifierConfig,
V extends Record<string, string> | Record<string, Record<string, string>> = {}
> = {
[P in keyof M]?: P extends keyof V
? MaybeBoolean<keyof M[P]> | (string & {}) // If both modifiers and variables define the same key, the value can be either a modifier or a string
: MaybeBoolean<keyof M[P]>;
};

export interface StencilConfig<
Expand Down Expand Up @@ -852,7 +857,7 @@ export type Stencil<
V extends Record<string, string> | Record<string, Record<string, string>>,
ID extends string = never
> = {
(modifiers?: ModifierValuesStencil<M> & VariableValuesStencil<V>): {
(modifiers?: ModifierValuesStencil<M, V> & VariableValuesStencil<V>): {
className: string;
style?: Record<string, string>;
};
Expand Down
40 changes: 40 additions & 0 deletions modules/styling/spec/cs.spec.tsx
Expand Up @@ -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<typeof myStencil>[0];
expectTypeOf<Arg>().toHaveProperty('width');
expectTypeOf<Arg['width']>().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}`);
});
});
});

Expand Down
34 changes: 34 additions & 0 deletions modules/styling/stories/Basics.stories.mdx
Expand Up @@ -313,6 +313,40 @@ Notice the stencil adds all the class names that match the base, modifiers, and

<ExampleCodeBlock code={CreateStencil} />

### 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)
Expand Down

0 comments on commit b2f8a99

Please sign in to comment.