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

feat: add Emotion renderer for v0 #13547

Merged
merged 10 commits into from
Jun 18, 2020
3 changes: 3 additions & 0 deletions packages/fluentui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Fixes
- Fix `Tooltip` layouting when it is closed @fealkhou ([#13237](https://github.com/microsoft/fluentui/pull/13237))

### Features
- Add Emotion as an optional CSS-in-JS renderer @layershifter ([#13547](https://github.com/microsoft/fluentui/pull/13547))

### Documentation
- Fix required version of CSB package, improve dependency generation for exported CodeSandboxes @layershifter ([#13637](https://github.com/microsoft/fluentui/pull/13637))

Expand Down
2 changes: 2 additions & 0 deletions packages/fluentui/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"@fluentui/react-component-ref": "^0.50.0",
"@fluentui/react-icons-northstar": "^0.50.0",
"@fluentui/react-northstar": "^0.50.0",
"@fluentui/react-northstar-fela-renderer": "^0.50.0",
"@fluentui/react-northstar-emotion-renderer": "^0.50.0",
"@fluentui/react-northstar-styles-renderer": "^0.50.0",
"@fluentui/react-telemetry": "^0.50.0",
"@fluentui/styles": "^0.50.0",
Expand Down
65 changes: 49 additions & 16 deletions packages/fluentui/docs/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import * as React from 'react';
import { hot } from 'react-hot-loader/root';
import { Provider, Debug, teamsTheme, teamsDarkTheme, teamsHighContrastTheme } from '@fluentui/react-northstar';
import {
Provider,
Debug,
teamsTheme,
teamsDarkTheme,
teamsHighContrastTheme,
RendererContext,
} from '@fluentui/react-northstar';
import { createEmotionRenderer } from '@fluentui/react-northstar-emotion-renderer';
import { createFelaRenderer } from '@fluentui/react-northstar-fela-renderer';
import { CreateRenderer } from '@fluentui/react-northstar-styles-renderer';
import { TelemetryPopover } from '@fluentui/react-telemetry';
import { mergeThemes } from '@fluentui/styles';

import { ThemeContext, ThemeContextData, themeContextDefaults } from './context/ThemeContext';
import { ThemeName, ThemeContext, ThemeContextData, themeContextDefaults } from './context/ThemeContext';
import Routes from './routes';

// Experimental dev-time accessibility attributes integrity validation.
Expand All @@ -21,18 +31,41 @@ const themes = {
teamsHighContrastTheme,
};

class App extends React.Component<any, ThemeContextData> {
function useRendererFactory(): CreateRenderer {
const rendererFactory = localStorage.fluentRenderer === 'emotion' ? createEmotionRenderer : createFelaRenderer;

React.useEffect(() => {
(window as any).setFluentRenderer = (rendererName: 'fela' | 'emotion') => {
if (rendererName === 'fela' || rendererName === 'emotion') {
localStorage.fluentRenderer = rendererName;
location.reload();
} else {
throw new Error('Only "emotion" & "fela" are supported!');
}
};
}, []);

return rendererFactory;
}

const App: React.FC = () => {
const [themeName, setThemeName] = React.useState<ThemeName>(themeContextDefaults.themeName);
// State also contains the updater function so it will
// be passed down into the context provider
state: ThemeContextData = {
...themeContextDefaults,
changeTheme: (e, { value: item }) => this.setState({ themeName: item.value }),
};

render() {
const { themeName } = this.state;
return (
<ThemeContext.Provider value={this.state}>
const themeContext = React.useMemo<ThemeContextData>(
() => ({
...themeContextDefaults,
changeTheme: (e, data) => setThemeName(data.value.value),
themeName,
}),
[themeName],
);

const rendererFactory = useRendererFactory();

return (
<ThemeContext.Provider value={themeContext}>
<RendererContext.Provider value={rendererFactory}>
<TelemetryPopover>
<Provider
as={React.Fragment}
Expand All @@ -50,9 +83,9 @@ class App extends React.Component<any, ThemeContextData> {
<Routes />
</Provider>
</TelemetryPopover>
</ThemeContext.Provider>
);
}
}
</RendererContext.Provider>
</ThemeContext.Provider>
);
};

export default hot(App);
2 changes: 1 addition & 1 deletion packages/fluentui/docs/src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

type ThemeName = 'teamsTheme' | 'teamsDarkTheme' | 'teamsHighContrastTheme';
export type ThemeName = 'teamsTheme' | 'teamsDarkTheme' | 'teamsHighContrastTheme';
type ThemeOption = { text: string; value: ThemeName };

export type ThemeContextData = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
coverage/
dist/
lib/
node_modules/
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["../../../scripts/eslint/index"],
"root": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@uifabric/build/gulp/.gulp');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = api => require('@uifabric/build/babel')(api);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../../../gulpfile';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
...require('@uifabric/build/jest'),
name: 'react-northstar-emotion-renderer',
moduleNameMapper: require('lerna-alias').jest({
directory: require('@uifabric/build/monorepo/findGitRoot')(),
}),
};
49 changes: 49 additions & 0 deletions packages/fluentui/react-northstar-emotion-renderer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@fluentui/react-northstar-emotion-renderer",
"description": "A CSS-in-JS renderer based on Emotion for FluentUI React Northstar.",
"version": "0.50.0",
"bugs": "https://github.com/microsoft/fluentui/issues",
"dependencies": {
"@babel/runtime": "^7.7.6",
"@emotion/cache": "^10.0.29",
"@emotion/serialize": "^0.11.16",
"@emotion/sheet": "^0.9.4",
"@emotion/utils": "^0.11.3",
"@fluentui/react-northstar-styles-renderer": "^0.50.0",
"@fluentui/styles": "^0.50.0",
"@quid/stylis-plugin-focus-visible": "^4.0.0",
"stylis-plugin-rtl": "^1.0.0"
},
"devDependencies": {
"@types/react": "16.8.25",
"@uifabric/build": "^7.0.0",
"lerna-alias": "^3.0.3-0",
"react": "16.8.6"
},
"files": [
"dist"
],
"homepage": "https://github.com/microsoft/fluentui/tree/master/packages/fluentui/react-northstar-emotion-renderer",
"jsnext:main": "dist/es/index.js",
"license": "MIT",
"main": "dist/commonjs/index.js",
"module": "dist/es/index.js",
"peerDependencies": {
"react": "^16.8.0",
"react-dom": "^16.8.0"
},
"publishConfig": {
"access": "public"
},
"repository": "microsoft/fluentui.git",
"scripts": {
"build": "gulp bundle:package:no-umd",
"clean": "gulp bundle:package:clean",
"lint": "eslint --ext .js,.ts,.tsx .",
"lint:fix": "yarn lint --fix",
"test": "gulp test",
"test:watch": "gulp test:watch"
},
"sideEffects": false,
"types": "dist/es/index.d.ts"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import createCache from '@emotion/cache';
import { ObjectInterpolation, serializeStyles } from '@emotion/serialize';
import { StyleSheet } from '@emotion/sheet';
import { EmotionCache, insertStyles } from '@emotion/utils';
import {
Renderer,
RendererRenderGlobal,
RendererRenderFont,
RendererRenderRule,
} from '@fluentui/react-northstar-styles-renderer';
// @ts-ignore No typings :(
import focusVisiblePlugin from '@quid/stylis-plugin-focus-visible';
// @ts-ignore No typings :(
import rtlPlugin from 'stylis-plugin-rtl';
import * as React from 'react';

import { disableAnimations } from './disableAnimations';
import { generateFontSource, getFontLocals, toCSSString } from './fontUtils';
import { invokeKeyframes } from './invokeKeyframes';

export function createEmotionRenderer(target?: Document): Renderer {
const cacheLtr = createCache({
container: target?.head,
key: 'fui',
stylisPlugins: [focusVisiblePlugin],

// TODO: make this configurable via perf flags
speedy: true,
}) as EmotionCache & { insert: Function };
const cacheRtl = createCache({
container: target?.head,
key: 'rfui',
stylisPlugins: [focusVisiblePlugin, rtlPlugin],

// TODO: make this configurable via perf flags
speedy: true,
});

const sheet = new StyleSheet({
key: `${cacheLtr.key}-global`,
nonce: cacheLtr.sheet.nonce,
container: cacheLtr.sheet.container,
});

const Provider: React.FC = props => {
// TODO: Find a way to cleanup global styles
// React.useEffect(() => {
// return () => sheet.flush();
// });

return <>{props.children}</>;
};

const renderRule: RendererRenderRule = (styles, param) => {
// Emotion has a bug with passing empty objects, should be fixed in upstream
if (Object.keys(styles).length === 0) {
return '';
}

const cache = param.direction === 'ltr' ? cacheLtr : cacheRtl;
const style = param.disableAnimations ? disableAnimations(styles) : styles;
const serialized = serializeStyles([invokeKeyframes(cache, style) as any], cache.registered, undefined);

insertStyles(cache, serialized, true);

return `${cache.key}-${serialized.name}`;
};

const renderGlobal: RendererRenderGlobal = (styles, selector) => {
if (typeof styles === 'string') {
const serializedStyles = serializeStyles(
[styles],
// This looks as a bug in typings as in Emotion code this function can be used with a single param.
// https://github.com/emotion-js/emotion/blob/a076e7fa5f78fec6515671b78801cfc9d6cf1316/packages/core/src/global.js#L45
// @ts-ignore
undefined,
);

cacheLtr.insert(``, serializedStyles, sheet, false);
}

if (typeof styles === 'object') {
if (typeof selector !== 'string') {
throw new Error('A valid "selector" is required when an object is passed to "renderGlobal"');
}

const serializedStyles = serializeStyles(
[{ [selector]: (styles as unknown) as ObjectInterpolation<{}> }],
// This looks as a bug in typings as in Emotion code this function can be used with a single param.
// https://github.com/emotion-js/emotion/blob/a076e7fa5f78fec6515671b78801cfc9d6cf1316/packages/core/src/global.js#L45
// @ts-ignore
null,
);

cacheLtr.insert(``, serializedStyles, sheet, false);
}
};
const renderFont: RendererRenderFont = font => {
const { localAlias, ...otherProperties } = font.props;

const fontLocals = getFontLocals(localAlias);
const fontFamily = toCSSString(font.name);

renderGlobal(
{
...otherProperties,
src: generateFontSource(font.paths, fontLocals),
fontFamily,
},
'@font-face',
);
};

return {
renderGlobal,
renderFont,
renderRule,

Provider,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ICSSInJSStyle } from '@fluentui/styles';
import { isStyleObject } from './utils';

const animationProps: (keyof ICSSInJSStyle)[] = [
'animation',
'animationName',
'animationDuration',
'animationTimingFunction',
'animationDelay',
'animationIterationCount',
'animationDirection',
'animationFillMode',
'animationPlayState',
];

export function disableAnimations(styles: ICSSInJSStyle): ICSSInJSStyle {
miroslavstastny marked this conversation as resolved.
Show resolved Hide resolved
for (const property in styles) {
if (animationProps.indexOf(property) !== -1) {
styles[property] = undefined;
} else if (isStyleObject(property)) {
styles[property] = disableAnimations(styles[property]);
}
}

return styles;
}
Loading