Skip to content

Commit

Permalink
chore: replace Fela with Emotion
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter committed Jun 16, 2020
1 parent 7223e76 commit 8a05bf0
Show file tree
Hide file tree
Showing 24 changed files with 529 additions and 28 deletions.
1 change: 1 addition & 0 deletions packages/fluentui/CHANGELOG.md
Expand Up @@ -100,6 +100,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `show` and `hide` animations in the `Popup` component @mnajdova ([#13439](https://github.com/microsoft/fluentui/pull/13439))
- Added icons: `MeetingTimeIcon`, `VideomailIcon`, `ScreenshareIcon`, `Shift24hIcon`, `ShiftActivity` @TanelVari ([#13513](https://github.com/microsoft/fluentui/pull/13513))
- Extract `Renderer` related types to a separate package @layershifter ([#13616](https://github.com/microsoft/fluentui/pull/13616))
- Add Emotion as an optional CSS-in-JS renderer @layershifter ([#13547](https://github.com/microsoft/fluentui/pull/13547))

### Performance
- Omit `_.findKey()` usage in `focusVisibleEnhancer()` @layershifter ([#13343](https://github.com/microsoft/fluentui/pull/13343))
Expand Down
2 changes: 2 additions & 0 deletions packages/fluentui/docs/package.json
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.49.0",
"@fluentui/react-northstar-emotion-renderer": "^0.49.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
@@ -1,14 +1,24 @@
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 { 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.
import { setup } from '@fluentui/ability-attributes';
import { CreateRenderer } from '../../react-northstar-styles-renderer/src';

// Temporarily disabling the validation for Screener.
if (process.env.NODE_ENV !== 'production' && !process.env.SCREENER) {
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
@@ -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
@@ -0,0 +1,4 @@
coverage/
dist/
lib/
node_modules/
@@ -0,0 +1,4 @@
{
"extends": ["../../../scripts/eslint/index"],
"root": true
}
@@ -0,0 +1 @@
module.exports = require('@uifabric/build/gulp/.gulp');
@@ -0,0 +1 @@
module.exports = api => require('@uifabric/build/babel')(api);
@@ -0,0 +1 @@
import '../../../gulpfile';
47 changes: 47 additions & 0 deletions packages/fluentui/react-northstar-emotion-renderer/package.json
@@ -0,0 +1,47 @@
{
"name": "@fluentui/react-northstar-emotion-renderer",
"description": "A CSS-in-JS renderer based on Emotion for FluentUI React Northstar.",
"version": "0.49.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.49.0",
"@fluentui/styles": "^0.49.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"
},
"sideEffects": false,
"types": "dist/es/index.d.ts"
}
@@ -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 React.createElement(React.Fragment, null, 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,
};
}
@@ -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 {
for (const property in styles) {
if (animationProps.indexOf(property) !== -1) {
styles[property] = undefined;
} else if (isStyleObject(property)) {
styles[property] = disableAnimations(styles[property]);
}
}

return styles;
}

0 comments on commit 8a05bf0

Please sign in to comment.