Skip to content

Commit

Permalink
feat(theming): add global style ability (#5825)
Browse files Browse the repository at this point in the history

Co-authored-by: Caleb Pollman <[email protected]>
  • Loading branch information
dbanksdesign and calebpollman authored Sep 24, 2024
1 parent c855f47 commit 3a677a1
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 86 deletions.
18 changes: 18 additions & 0 deletions .changeset/red-fans-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@aws-amplify/ui-react": patch
"@aws-amplify/ui": patch
---

feat(theming): add global style ability (experimental)

Adding the ability to create global styles with the experimental theming APIs

```jsx
<GlobalStyle styles={{
'body': {
backgroundColor: 'purple'
// supports design tokens!
color: theme.tokens.colors.font.primary
}
}} />
```
1 change: 1 addition & 0 deletions packages/react/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ exports[`@aws-amplify/ui-react/internal exports should match snapshot 1`] = `
exports[`@aws-amplify/ui-react/server exports should match snapshot 1`] = `
[
"ComponentStyle",
"GlobalStyle",
"ThemeStyle",
"createComponentClasses",
"createTheme",
Expand Down
37 changes: 11 additions & 26 deletions packages/react/src/components/ThemeProvider/ComponentStyle.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import * as React from 'react';
import { WebTheme, createComponentCSS } from '@aws-amplify/ui';
import {
BaseComponentProps,
ElementType,
ForwardRefPrimitive,
Primitive,
PrimitiveProps,
} from '../../primitives/types';
import { primitiveWithForwardRef } from '../../primitives/utils/primitiveWithForwardRef';
import { BaseComponentTheme } from '@aws-amplify/ui';
import { Style } from './Style';

interface BaseComponentStyleProps extends BaseComponentProps {
interface ComponentStyleProps extends React.ComponentProps<'style'> {
/**
* Provide a server generated nonce which matches your CSP `style-src` rule.
* This will be attached to the generated <style> tag.
Expand All @@ -22,13 +14,15 @@ interface BaseComponentStyleProps extends BaseComponentProps {
componentThemes: BaseComponentTheme[];
}

export type ComponentStyleProps<Element extends ElementType = 'style'> =
PrimitiveProps<BaseComponentStyleProps, Element>;

const ComponentStylePrimitive: Primitive<ComponentStyleProps, 'style'> = (
{ theme, componentThemes = [], ...rest },
ref
) => {
/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const ComponentStyle = ({
theme,
componentThemes = [],
...rest
}: ComponentStyleProps): JSX.Element | null => {
if (!theme || !componentThemes.length) {
return null;
}
Expand All @@ -38,16 +32,7 @@ const ComponentStylePrimitive: Primitive<ComponentStyleProps, 'style'> = (
components: componentThemes,
});

return <Style {...rest} ref={ref} cssText={cssText} />;
return <Style {...rest} cssText={cssText} />;
};

/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const ComponentStyle: ForwardRefPrimitive<
BaseComponentStyleProps,
'style'
> = primitiveWithForwardRef(ComponentStylePrimitive);

ComponentStyle.displayName = 'ComponentStyle';
32 changes: 32 additions & 0 deletions packages/react/src/components/ThemeProvider/GlobalStyle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { createGlobalCSS } from '@aws-amplify/ui';
import { Style } from './Style';

interface GlobalStyleProps extends React.ComponentProps<'style'> {
/**
* Provide a server generated nonce which matches your CSP `style-src` rule.
* This will be attached to the generated <style> tag.
* @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
*/
nonce?: string;
styles: Parameters<typeof createGlobalCSS>[0];
}

/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const GlobalStyle = ({
styles,
...rest
}: GlobalStyleProps): JSX.Element | null => {
if (!styles) {
return null;
}

const cssText = createGlobalCSS(styles);

return <Style {...rest} cssText={cssText} />;
};

GlobalStyle.displayName = 'GlobalStyle';
34 changes: 7 additions & 27 deletions packages/react/src/components/ThemeProvider/Style.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
import * as React from 'react';
import {
BaseComponentProps,
ElementType,
ForwardRefPrimitive,
Primitive,
PrimitiveProps,
} from '../../primitives/types';
import { primitiveWithForwardRef } from '../../primitives/utils/primitiveWithForwardRef';

interface BaseStyleProps extends BaseComponentProps {
interface StyleProps extends React.ComponentProps<'style'> {
cssText?: string;
}

export type StyleProps<Element extends ElementType = 'style'> = PrimitiveProps<
BaseStyleProps,
Element
>;

const StylePrimitive: Primitive<StyleProps, 'style'> = (
{ cssText, ...rest },
ref
) => {
/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const Style = ({ cssText, ...rest }: StyleProps): JSX.Element | null => {
/*
Only inject theme CSS variables if given a theme.
The CSS file users import already has the default theme variables in it.
Expand Down Expand Up @@ -69,22 +57,14 @@ const StylePrimitive: Primitive<StyleProps, 'style'> = (
if (cssText === undefined || /<\/style/i.test(cssText)) {
return null;
}

return (
<style
{...rest}
ref={ref}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: cssText }}
/>
);
};

/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const Style: ForwardRefPrimitive<BaseStyleProps, 'style'> =
primitiveWithForwardRef(StylePrimitive);

Style.displayName = 'Style';
42 changes: 10 additions & 32 deletions packages/react/src/components/ThemeProvider/ThemeStyle.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import * as React from 'react';
import { WebTheme } from '@aws-amplify/ui';
import {
BaseComponentProps,
ElementType,
ForwardRefPrimitive,
Primitive,
PrimitiveProps,
} from '../../primitives/types';
import { primitiveWithForwardRef } from '../../primitives/utils/primitiveWithForwardRef';
import { Style } from './Style';

interface BaseStyleThemeProps extends BaseComponentProps {
interface ThemeStyleProps extends React.ComponentProps<'style'> {
/**
* Provide a server generated nonce which matches your CSP `style-src` rule.
* This will be attached to the generated <style> tag.
Expand All @@ -20,32 +12,18 @@ interface BaseStyleThemeProps extends BaseComponentProps {
theme?: WebTheme;
}

export type ThemeStyleProps<Element extends ElementType = 'style'> =
PrimitiveProps<BaseStyleThemeProps, Element>;

const ThemeStylePrimitive: Primitive<ThemeStyleProps, 'style'> = (
{ theme, nonce, ...rest },
ref
) => {
if (!theme) return null;

const { name, cssText } = theme;
return (
<Style
{...rest}
ref={ref}
cssText={cssText}
nonce={nonce}
id={`amplify-theme-${name}`}
/>
);
};

/**
* @experimental
* [📖 Docs](https://ui.docs.amplify.aws/react/components/theme)
*/
export const ThemeStyle: ForwardRefPrimitive<BaseStyleThemeProps, 'style'> =
primitiveWithForwardRef(ThemeStylePrimitive);
export const ThemeStyle = ({
theme,
...rest
}: ThemeStyleProps): JSX.Element | null => {
if (!theme) return null;

const { name, cssText } = theme;
return <Style {...rest} cssText={cssText} id={`amplify-theme-${name}`} />;
};

ThemeStyle.displayName = 'ThemeStyle';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { render } from '@testing-library/react';
import * as React from 'react';

import { GlobalStyle } from '../GlobalStyle';

describe('GlobalStyle', () => {
it('does not render anything if no theme is passed', async () => {
// @ts-expect-error - missing props
const { container } = render(<GlobalStyle />);

const styleTag = container.querySelector(`style`);
expect(styleTag).toBe(null);
});

it('renders a style tag if styles are passed', async () => {
const { container } = render(
<GlobalStyle
styles={{
'.foo': {
backgroundColor: 'red',
},
}}
/>
);

const styleTag = container.querySelector(`style`);
expect(styleTag).toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions packages/react/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { ThemeStyle } from './components/ThemeProvider/ThemeStyle';
export { ComponentStyle } from './components/ThemeProvider/ComponentStyle';
export { GlobalStyle } from './components/ThemeProvider/GlobalStyle';
export {
createTheme,
defineComponentTheme,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`@aws-amplify/ui createGlobalCSS should handle psuedo-states 1`] = `
".bar:active { background-color:#ff9900; }
.bar { background-color:#bada55; }
"
`;

exports[`@aws-amplify/ui createGlobalCSS should work with design tokens 1`] = `
".bar:active { background-color:var(--amplify-colors-blue-40); }
.bar { background-color:var(--amplify-colors-blue-20); }
"
`;

exports[`@aws-amplify/ui createGlobalCSS should work with regular styles 1`] = `
".foo { background-color:red; }
button > .icon { color:blueviolet; }
"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createGlobalCSS } from '../createGlobalCSS';
import { createTheme } from '../createTheme';

const { tokens } = createTheme();

describe('@aws-amplify/ui', () => {
describe('createGlobalCSS', () => {
it('should work with regular styles', () => {
expect(
createGlobalCSS({
'.foo': {
backgroundColor: 'red',
},
'button > .icon': {
color: 'blueviolet',
},
})
).toMatchSnapshot();
});
it('should handle psuedo-states', () => {
expect(
createGlobalCSS({
'.bar': {
backgroundColor: '#bada55',
':active': {
backgroundColor: '#ff9900',
},
},
})
).toMatchSnapshot();
});
it('should work with design tokens', () => {
expect(
createGlobalCSS({
'.bar': {
backgroundColor: tokens.colors.blue[20],
':active': {
backgroundColor: tokens.colors.blue[40],
},
},
})
).toMatchSnapshot();
});
});
});
2 changes: 1 addition & 1 deletion packages/ui/src/theme/createTheme/createComponentCSS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function addVars(selector: string, vars: Record<string, unknown>) {
.join(' ')}}\n`;
}

function recursiveComponentCSS(baseSelector: string, theme: BaseTheme) {
export function recursiveComponentCSS(baseSelector: string, theme: BaseTheme) {
let str = '';
const { _modifiers = {}, _element = {}, _vars, ...props } = theme;

Expand Down
14 changes: 14 additions & 0 deletions packages/ui/src/theme/createTheme/createGlobalCSS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ComponentStyles } from '../components/utils';
import { recursiveComponentCSS } from './createComponentCSS';

type GlobalCSS = Record<string, ComponentStyles>;

export function createGlobalCSS(css: GlobalCSS) {
let cssText = ``;

for (const [selector, styles] of Object.entries(css)) {
cssText += recursiveComponentCSS(selector, styles);
}

return cssText;
}
1 change: 1 addition & 0 deletions packages/ui/src/theme/createTheme/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { createTheme } from './createTheme';
export { defineComponentTheme } from './defineComponentTheme';
export { createComponentCSS } from './createComponentCSS';
export { createGlobalCSS } from './createGlobalCSS';
export {
cssNameTransform,
setupTokens,
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
defineComponentTheme,
createComponentClasses,
createComponentCSS,
createGlobalCSS,
cssNameTransform,
isDesignToken,
setupTokens,
Expand Down

0 comments on commit 3a677a1

Please sign in to comment.