Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP mechanism for creating styled with a custom theme w/o augmenting DefaultTheme #4298

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/styled-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"tslib": "2.6.2"
},
"peerDependencies": {
"@types/react": ">= 16.8.0",
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
},
Expand Down
53 changes: 31 additions & 22 deletions packages/styled-components/src/constructors/constructWithOptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Attrs,
BaseObject,
DefaultTheme,
Interpolation,
IStyledComponent,
IStyledComponentFactory,
Expand All @@ -16,13 +17,13 @@ import { EMPTY_OBJECT } from '../utils/empties';
import styledError from '../utils/error';
import css from './css';

type AttrsResult<T extends Attrs<any>> = T extends (...args: any) => infer P
type AttrsResult<T extends Attrs<any, any>> = T extends (...args: any) => infer P
? P extends object
? P
: never
: T extends object
? T
: never;
? T
: never;

/**
* Extract non-optional fields from given object type.
Expand All @@ -37,14 +38,15 @@ type RequiredFields<T, Ex> = Pick<
export interface Styled<
R extends Runtime,
Target extends StyledTarget<R>,
OuterProps extends object,
Theme extends object = DefaultTheme,
OuterProps extends object = BaseObject,
OuterStatics extends object = BaseObject,
InnerProps extends object = OuterProps,
> {
<Props extends object = BaseObject, Statics extends object = BaseObject>(
initialStyles: Styles<Substitute<InnerProps, NoInfer<Props>>>,
...interpolations: Interpolation<Substitute<InnerProps, NoInfer<Props>>>[]
): IStyledComponent<R, Substitute<OuterProps, Props>> &
initialStyles: Styles<Substitute<InnerProps, NoInfer<Props>>, Theme>,
...interpolations: Interpolation<Substitute<InnerProps, NoInfer<Props>>, Theme>[]
): IStyledComponent<R, Substitute<OuterProps, Props>, Theme> &
OuterStatics &
Statics &
(R extends 'web'
Expand All @@ -56,12 +58,13 @@ export interface Styled<
attrs: <
Props extends object = BaseObject,
PrivateMergedProps extends object = Substitute<OuterProps, Props>,
PrivateAttrsArg extends Attrs<PrivateMergedProps> = Attrs<PrivateMergedProps>,
PrivateAttrsArg extends Attrs<PrivateMergedProps, Theme> = Attrs<PrivateMergedProps, Theme>,
>(
attrs: PrivateAttrsArg
) => StyledAttrsResult<
R,
Target,
Theme,
OuterProps,
OuterStatics,
InnerProps,
Expand All @@ -70,18 +73,21 @@ export interface Styled<
PrivateAttrsArg
>;

withConfig: (config: StyledOptions<R, OuterProps>) => Styled<R, Target, OuterProps, OuterStatics>;
withConfig: (
config: StyledOptions<R, OuterProps, Theme>
) => Styled<R, Target, Theme, OuterProps, OuterStatics>;
}

type StyledAttrsResult<
R extends Runtime,
Target extends StyledTarget<R>,
OuterProps extends object,
Theme extends object = DefaultTheme,
OuterProps extends object = BaseObject,
OuterStatics extends object = BaseObject,
InnerProps extends object = OuterProps,
Props extends object = BaseObject,
PrivateMergedProps extends object = Substitute<OuterProps, Props>,
PrivateAttrsArg extends Attrs<PrivateMergedProps> = Attrs<PrivateMergedProps>,
PrivateAttrsArg extends Attrs<PrivateMergedProps, Theme> = Attrs<PrivateMergedProps, Theme>,
> = (
AttrsResult<PrivateAttrsArg> extends { as: infer RuntimeTarget extends KnownTarget }
? {
Expand All @@ -96,6 +102,7 @@ type StyledAttrsResult<
? Styled<
R,
PrivateResolvedTarget,
Theme,
PrivateResolvedTarget extends KnownTarget
? Substitute<TargetProps, Props & Partial<RequiredFields<PrivateAttrsArg, 'as'>>>
: PrivateMergedProps,
Expand All @@ -112,16 +119,17 @@ type StyledAttrsResult<
export default function constructWithOptions<
R extends Runtime,
Target extends StyledTarget<R>,
Theme extends object = DefaultTheme,
OuterProps extends object = Target extends KnownTarget
? React.ComponentPropsWithRef<Target>
: BaseObject,
OuterStatics extends object = BaseObject,
InnerProps extends object = OuterProps,
>(
componentConstructor: IStyledComponentFactory<R, StyledTarget<R>, object, any>,
componentConstructor: IStyledComponentFactory<R, StyledTarget<R>, object, any, Theme>,
tag: StyledTarget<R>,
options: StyledOptions<R, OuterProps> = EMPTY_OBJECT
): Styled<R, Target, OuterProps, OuterStatics, InnerProps> {
options: StyledOptions<R, OuterProps, Theme> = EMPTY_OBJECT
): Styled<R, Target, Theme, OuterProps, OuterStatics, InnerProps> {
/**
* We trust that the tag is a valid component as long as it isn't
* falsish. Typically the tag here is a string or function (i.e.
Expand All @@ -135,13 +143,13 @@ export default function constructWithOptions<

/* This is callable directly as a template function */
const templateFunction = <Props extends object = BaseObject, Statics extends object = BaseObject>(
initialStyles: Styles<Substitute<InnerProps, Props>>,
...interpolations: Interpolation<Substitute<InnerProps, Props>>[]
initialStyles: Styles<Substitute<InnerProps, Props>, Theme>,
...interpolations: Interpolation<Substitute<InnerProps, Props>, Theme>[]
) =>
componentConstructor<Substitute<InnerProps, Props>, Statics>(
tag,
options as StyledOptions<R, Substitute<InnerProps, Props>>,
css<Substitute<InnerProps, Props>>(initialStyles, ...interpolations)
options as StyledOptions<R, Substitute<InnerProps, Props>, Theme>,
css<Substitute<InnerProps, Props>, Theme>(initialStyles, ...interpolations)
);

/**
Expand All @@ -153,20 +161,21 @@ export default function constructWithOptions<
templateFunction.attrs = <
Props extends object = BaseObject,
PrivateMergedProps extends object = Substitute<OuterProps, Props>,
PrivateAttrsArg extends Attrs<PrivateMergedProps> = Attrs<PrivateMergedProps>,
PrivateAttrsArg extends Attrs<PrivateMergedProps, Theme> = Attrs<PrivateMergedProps, Theme>,
>(
attrs: PrivateAttrsArg
): StyledAttrsResult<
R,
Target,
Theme,
OuterProps,
OuterStatics,
InnerProps,
Props,
PrivateMergedProps,
PrivateAttrsArg
> =>
constructWithOptions<R, Target, any, any, any>(componentConstructor, tag, {
constructWithOptions<R, Target, Theme, any, any, any>(componentConstructor, tag, {
...options,
attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean),
}) as any;
Expand All @@ -175,8 +184,8 @@ export default function constructWithOptions<
* If config methods are called, wrap up a new template function
* and merge options.
*/
templateFunction.withConfig = (config: StyledOptions<R, OuterProps>) =>
constructWithOptions<R, Target, OuterProps, OuterStatics>(componentConstructor, tag, {
templateFunction.withConfig = (config: StyledOptions<R, OuterProps, Theme>) =>
constructWithOptions<R, Target, Theme, OuterProps, OuterStatics>(componentConstructor, tag, {
...options,
...config,
});
Expand Down
28 changes: 14 additions & 14 deletions packages/styled-components/src/constructors/createGlobalStyle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { Context } from 'react';
import { STATIC_EXECUTION_CONTEXT } from '../constants';
import GlobalStyle from '../models/GlobalStyle';
import { useStyleSheetContext } from '../models/StyleSheetManager';
Expand All @@ -10,21 +10,21 @@ import determineTheme from '../utils/determineTheme';
import generateComponentId from '../utils/generateComponentId';
import css from './css';

export default function createGlobalStyle<Props extends object>(
strings: Styles<Props>,
...interpolations: Array<Interpolation<Props>>
) {
const rules = css<Props>(strings, ...interpolations);
export default function createGlobalStyle<
Props extends object,
Theme extends object = DefaultTheme,
>(strings: Styles<Props, Theme>, ...interpolations: Array<Interpolation<Props, Theme>>) {
const rules = css<Props, Theme>(strings, ...interpolations);
const styledComponentId = `sc-global-${generateComponentId(JSON.stringify(rules))}`;
const globalStyle = new GlobalStyle<Props>(rules, styledComponentId);
const globalStyle = new GlobalStyle<Props, Theme>(rules, styledComponentId);

if (process.env.NODE_ENV !== 'production') {
checkDynamicCreation(styledComponentId);
}

const GlobalStyleComponent: React.ComponentType<ExecutionProps & Props> = props => {
const GlobalStyleComponent: React.ComponentType<ExecutionProps<Theme> & Props> = props => {
const ssc = useStyleSheetContext();
const theme = React.useContext(ThemeContext);
const theme = React.useContext<Theme | undefined>(ThemeContext as Context<Theme | undefined>);
const instanceRef = React.useRef(ssc.styleSheet.allocateGSInstance(styledComponentId));

const instance = instanceRef.current;
Expand Down Expand Up @@ -62,23 +62,23 @@ export default function createGlobalStyle<Props extends object>(

function renderStyles(
instance: number,
props: ExecutionProps,
props: ExecutionProps<Theme>,
styleSheet: StyleSheet,
theme: DefaultTheme | undefined,
theme: Theme | undefined,
stylis: Stringifier
) {
if (globalStyle.isStatic) {
globalStyle.renderStyles(
instance,
STATIC_EXECUTION_CONTEXT as unknown as ExecutionContext & Props,
STATIC_EXECUTION_CONTEXT as unknown as ExecutionContext<Theme> & Props,
styleSheet,
stylis
);
} else {
const context = {
...props,
theme: determineTheme(props, theme, GlobalStyleComponent.defaultProps),
} as ExecutionContext & Props;
theme: determineTheme<Theme>(props, theme, GlobalStyleComponent.defaultProps),
} as ExecutionContext<Theme> & Props;

globalStyle.renderStyles(instance, context, styleSheet, stylis);
}
Expand Down
32 changes: 16 additions & 16 deletions packages/styled-components/src/constructors/css.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BaseObject,
DefaultTheme,
Interpolation,
NoInfer,
RuleSet,
Expand All @@ -17,27 +18,24 @@ import isPlainObject from '../utils/isPlainObject';
* Used when flattening object styles to determine if we should
* expand an array of styles.
*/
const addTag = <T extends RuleSet<any>>(arg: T): T & { isCss: true } =>
const addTag = <T extends RuleSet<any, any>>(arg: T): T & { isCss: true } =>
Object.assign(arg, { isCss: true } as const);

function css(styles: Styles<object>, ...interpolations: Interpolation<object>[]): RuleSet<object>;
function css<Props extends object>(
styles: Styles<NoInfer<Props>>,
...interpolations: Interpolation<NoInfer<Props>>[]
): RuleSet<NoInfer<Props>>;
function css<Props extends object = BaseObject>(
styles: Styles<NoInfer<Props>>,
...interpolations: Interpolation<NoInfer<Props>>[]
): RuleSet<NoInfer<Props>> {
function css<Props extends object = BaseObject, Theme extends object = DefaultTheme>(
styles: Styles<NoInfer<Props>, Theme>,
...interpolations: Interpolation<NoInfer<Props>, Theme>[]
): RuleSet<NoInfer<Props>, Theme> {
if (isFunction(styles) || isPlainObject(styles)) {
const styleFunctionOrObject = styles as StyleFunction<Props> | StyledObject<Props>;
const styleFunctionOrObject = styles as
| StyleFunction<Props, Theme>
| StyledObject<Props, Theme>;

return addTag(
flatten<Props>(
interleave<Props>(EMPTY_ARRAY, [
flatten<Props, Theme>(
interleave<Props, Theme>(EMPTY_ARRAY, [
styleFunctionOrObject,
...interpolations,
]) as Interpolation<object>
]) as Interpolation<Props, Theme>
)
);
}
Expand All @@ -49,11 +47,13 @@ function css<Props extends object = BaseObject>(
styleStringArray.length === 1 &&
typeof styleStringArray[0] === 'string'
) {
return flatten<Props>(styleStringArray);
return flatten<Props, Theme>(styleStringArray);
}

return addTag(
flatten<Props>(interleave<Props>(styleStringArray, interpolations) as Interpolation<object>)
flatten<Props, Theme>(
interleave<Props, Theme>(styleStringArray, interpolations) as Interpolation<Props, Theme>
)
);
}

Expand Down
12 changes: 6 additions & 6 deletions packages/styled-components/src/constructors/keyframes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Keyframes from '../models/Keyframes';
import { Interpolation, Styles } from '../types';
import { BaseObject, Interpolation, Styles } from '../types';
import generateComponentId from '../utils/generateComponentId';
import { joinStringArray } from '../utils/joinStrings';
import css from './css';

export default function keyframes<Props extends object = {}>(
strings: Styles<Props>,
...interpolations: Array<Interpolation<Props>>
): Keyframes {
export default function keyframes<
Props extends object = BaseObject,
Theme extends object = BaseObject,
>(strings: Styles<Props, Theme>, ...interpolations: Array<Interpolation<Props, Theme>>): Keyframes {
/* Warning if you've used keyframes on React Native */
if (
process.env.NODE_ENV !== 'production' &&
Expand All @@ -19,7 +19,7 @@ export default function keyframes<Props extends object = {}>(
);
}

const rules = joinStringArray(css<Props>(strings, ...interpolations) as string[]);
const rules = joinStringArray(css<Props, Theme>(strings, ...interpolations) as string[]);
const name = generateComponentId(rules);
return new Keyframes(name, rules);
}
8 changes: 4 additions & 4 deletions packages/styled-components/src/constructors/styled.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import createStyledComponent from '../models/StyledComponent';
import { WebTarget } from '../types';
import { DefaultTheme, WebTarget } from '../types';
import domElements, { SupportedHTMLElements } from '../utils/domElements';
import constructWithOptions, { Styled } from './constructWithOptions';

const baseStyled = <Target extends WebTarget>(tag: Target) =>
constructWithOptions<'web', Target>(createStyledComponent, tag);
const baseStyled = <Target extends WebTarget, Theme extends object = DefaultTheme>(tag: Target) =>
constructWithOptions<'web', Target, Theme>(createStyledComponent, tag);

const styled = baseStyled as typeof baseStyled & {
[E in SupportedHTMLElements]: Styled<'web', E, JSX.IntrinsicElements[E]>;
[E in SupportedHTMLElements]: Styled<'web', E, DefaultTheme, JSX.IntrinsicElements[E]>;
};

// Shorthands for all valid HTML Elements
Expand Down
10 changes: 6 additions & 4 deletions packages/styled-components/src/hoc/withTheme.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import { ThemeContext } from '../models/ThemeProvider';
import { DefaultTheme, ThemeContext } from '../models/ThemeProvider';
import { AnyComponent, ExecutionProps } from '../types';
import determineTheme from '../utils/determineTheme';
import getComponentName from '../utils/getComponentName';
import hoist from '../utils/hoist';

export default function withTheme<T extends AnyComponent>(Component: T) {
const WithTheme = React.forwardRef<T, JSX.LibraryManagedAttributes<T, ExecutionProps>>(
export default function withTheme<T extends AnyComponent, Theme extends object = DefaultTheme>(
Component: T
) {
const WithTheme = React.forwardRef<T, JSX.LibraryManagedAttributes<T, ExecutionProps<Theme>>>(
(props, ref) => {
const theme = React.useContext(ThemeContext);
const theme = React.useContext(ThemeContext) as Theme | undefined;
const themeProp = determineTheme(props, theme, Component.defaultProps);

if (process.env.NODE_ENV !== 'production' && themeProp === undefined) {
Expand Down