diff --git a/packages/fluentui/CHANGELOG.md b/packages/fluentui/CHANGELOG.md index b3485c8795bf0f..63a80c89c1f325 100644 --- a/packages/fluentui/CHANGELOG.md +++ b/packages/fluentui/CHANGELOG.md @@ -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)) diff --git a/packages/fluentui/docs/package.json b/packages/fluentui/docs/package.json index f7e0755eec338a..7cb79a93f7c67d 100644 --- a/packages/fluentui/docs/package.json +++ b/packages/fluentui/docs/package.json @@ -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", diff --git a/packages/fluentui/docs/src/app.tsx b/packages/fluentui/docs/src/app.tsx index 5df2464e9fb171..de47fbfa9edb2f 100644 --- a/packages/fluentui/docs/src/app.tsx +++ b/packages/fluentui/docs/src/app.tsx @@ -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. @@ -21,18 +31,41 @@ const themes = { teamsHighContrastTheme, }; -class App extends React.Component { +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(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 ( - + const themeContext = React.useMemo( + () => ({ + ...themeContextDefaults, + changeTheme: (e, data) => setThemeName(data.value.value), + themeName, + }), + [themeName], + ); + + const rendererFactory = useRendererFactory(); + + return ( + + { - - ); - } -} + + + ); +}; export default hot(App); diff --git a/packages/fluentui/docs/src/context/ThemeContext.tsx b/packages/fluentui/docs/src/context/ThemeContext.tsx index c34ac804b30104..778183f0ce035e 100644 --- a/packages/fluentui/docs/src/context/ThemeContext.tsx +++ b/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 = { diff --git a/packages/fluentui/react-northstar-emotion-renderer/.eslintignore b/packages/fluentui/react-northstar-emotion-renderer/.eslintignore new file mode 100644 index 00000000000000..b08f6bfbd8d7fc --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/.eslintignore @@ -0,0 +1,4 @@ +coverage/ +dist/ +lib/ +node_modules/ diff --git a/packages/fluentui/react-northstar-emotion-renderer/.eslintrc.json b/packages/fluentui/react-northstar-emotion-renderer/.eslintrc.json new file mode 100644 index 00000000000000..853e09ad6c3f2b --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../../scripts/eslint/index"], + "root": true +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/.gulp.js b/packages/fluentui/react-northstar-emotion-renderer/.gulp.js new file mode 100644 index 00000000000000..fc15ae19f15311 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/.gulp.js @@ -0,0 +1 @@ +module.exports = require('@uifabric/build/gulp/.gulp'); diff --git a/packages/fluentui/react-northstar-emotion-renderer/babel.config.js b/packages/fluentui/react-northstar-emotion-renderer/babel.config.js new file mode 100644 index 00000000000000..12a35d5c90c44e --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/babel.config.js @@ -0,0 +1 @@ +module.exports = api => require('@uifabric/build/babel')(api); diff --git a/packages/fluentui/react-northstar-emotion-renderer/gulpfile.ts b/packages/fluentui/react-northstar-emotion-renderer/gulpfile.ts new file mode 100644 index 00000000000000..1494ed38c24558 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/gulpfile.ts @@ -0,0 +1 @@ +import '../../../gulpfile'; diff --git a/packages/fluentui/react-northstar-emotion-renderer/jest.config.js b/packages/fluentui/react-northstar-emotion-renderer/jest.config.js new file mode 100644 index 00000000000000..5a79abb5ccb2b4 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/jest.config.js @@ -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')(), + }), +}; diff --git a/packages/fluentui/react-northstar-emotion-renderer/package.json b/packages/fluentui/react-northstar-emotion-renderer/package.json new file mode 100644 index 00000000000000..a23d60e8e67e6e --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/package.json @@ -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" +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/createEmotionRenderer.tsx b/packages/fluentui/react-northstar-emotion-renderer/src/createEmotionRenderer.tsx new file mode 100644 index 00000000000000..0c25c413396ea1 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/createEmotionRenderer.tsx @@ -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, + }; +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/disableAnimations.ts b/packages/fluentui/react-northstar-emotion-renderer/src/disableAnimations.ts new file mode 100644 index 00000000000000..a2515ff8cba55a --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/disableAnimations.ts @@ -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; +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/fontUtils.ts b/packages/fluentui/react-northstar-emotion-renderer/src/fontUtils.ts new file mode 100644 index 00000000000000..fec15f2bd1dfda --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/fontUtils.ts @@ -0,0 +1,126 @@ +// +// Code here is taken mainly from fela-utils to implement to same FontFace interface. +// + +export function getFontLocals(localAlias?: string | string[]): string[] { + if (typeof localAlias === 'string') { + return [localAlias]; + } + + if (Array.isArray(localAlias)) { + return localAlias.slice(); + } + + return []; +} + +export function toCSSString(value: string): string { + if (value.charAt(0) === '"') { + return value; + } + + return `"${value}"`; +} + +function isBase64(property: string): boolean { + return property.substr(0, 5) === 'data:'; +} + +function getFontUrl(src: string): string { + if (isBase64(src)) { + return src; + } + + return `'${src}'`; +} + +const formats: Record = { + '.woff': 'woff', + '.woff2': 'woff2', + '.eot': 'embedded-opentype', + '.ttf': 'truetype', + '.otf': 'opentype', + '.svg': 'svg', + '.svgz': 'svg', +}; + +const base64Formats: Record = { + 'image/svg+xml': 'svg', + 'application/x-font-woff': 'woff', + 'application/font-woff': 'woff', + 'application/x-font-woff2': 'woff2', + 'application/font-woff2': 'woff2', + 'font/woff2': 'woff2', + 'application/octet-stream': 'truetype', + 'application/x-font-ttf': 'truetype', + 'application/x-font-truetype': 'truetype', + 'application/x-font-opentype': 'opentype', + 'application/vnd.ms-fontobject': 'embedded-opentype', + 'application/font-sfnt': 'sfnt', +}; + +function getFontFormat(src: string): string { + if (isBase64(src)) { + let mime = ''; + for (let i = 5; ; i++) { + // 'data:'.length === 5 + const c = src.charAt(i); + + if (c === ';' || c === ',') { + break; + } + + mime += c; + } + + const fmt = base64Formats[mime]; + if (fmt) { + return fmt; + } + + console.warn( + `A invalid base64 font was used. Please use one of the following mime type: ${Object.keys(base64Formats).join( + ', ', + )}.`, + ); + } else { + let extension = ''; + for (let i = src.length - 1; ; i--) { + const c = src.charAt(i); + + if (c === '.') { + extension = c + extension; + break; + } + + extension = c + extension; + } + + const fmt = formats[extension]; + if (fmt) { + return fmt; + } + + console.warn(`A invalid font-format was used in "${src}". Use one of these: ${Object.keys(formats).join(', ')}.`); + } + return ''; +} + +export function generateFontSource(files: string[] = [], fontLocals: string[] = []): string { + const localSource = fontLocals.reduce((src, local, index) => { + const prefix = index > 0 ? ',' : ''; + const localUrl = getFontUrl(local); + + return `${src}${prefix}local(${localUrl})`; + }, ''); + const urlSource = files.reduce((src, fileSource, index) => { + const prefix = index > 0 ? ',' : ''; + const fileFormat = getFontFormat(fileSource); + const fileUrl = getFontUrl(fileSource); + + return `${src}${prefix}url(${fileUrl}) format('${fileFormat}')`; + }, ''); + const delimiter = localSource.length > 0 && urlSource.length > 0 ? ',' : ''; + + return `${localSource}${delimiter}${urlSource}`; +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/index.ts b/packages/fluentui/react-northstar-emotion-renderer/src/index.ts new file mode 100644 index 00000000000000..66a235c66a06bd --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/index.ts @@ -0,0 +1 @@ +export { createEmotionRenderer } from './createEmotionRenderer'; diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/invokeKeyframes.ts b/packages/fluentui/react-northstar-emotion-renderer/src/invokeKeyframes.ts new file mode 100644 index 00000000000000..b2296e4ed305fa --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/invokeKeyframes.ts @@ -0,0 +1,51 @@ +import { serializeStyles } from '@emotion/serialize'; +import { EmotionCache } from '@emotion/utils'; +import { AnimationName, ICSSInJSStyle } from '@fluentui/styles'; + +import { isStyleObject } from './utils'; + +// This code is taken from @emotion/core: +// https://github.com/emotion-js/emotion/blob/a076e7fa5f78fec6515671b78801cfc9d6cf1316/packages/core/src/keyframes.js#L11 +const keyframes = ( + cache: EmotionCache, + keyframe: AnimationName['keyframe'], + params: AnimationName['params'] = {}, +): object => { + const style = typeof keyframe === 'function' ? keyframe(params) : keyframe; + const insertable = serializeStyles([style as any], cache.registered, undefined); + + const name = `animation-${insertable.name}`; + const styles = `@keyframes ${name}{${insertable.styles}}`; + + return { + name, + styles, + anim: 1, + toString() { + return `_EMO_${name}_${styles}_EMO_`; + }, + }; +}; + +/** + * Allows to transform animations that are defined under "animationName" to keyframes. + */ +export function invokeKeyframes(cache: EmotionCache, styles: ICSSInJSStyle) { + for (const property in styles) { + if (isStyleObject(styles[property])) { + if (property === 'animationName') { + const style = styles[property] as AnimationName; + + if (style.keyframe) { + styles[property] = keyframes(cache, style.keyframe, style.params); + } + + continue; + } + + styles[property] = invokeKeyframes(cache, styles[property]); + } + } + + return styles; +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/src/utils.ts b/packages/fluentui/react-northstar-emotion-renderer/src/utils.ts new file mode 100644 index 00000000000000..677f08d3002c1f --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/src/utils.ts @@ -0,0 +1,5 @@ +import { ICSSInJSStyle } from '@fluentui/styles'; + +export function isStyleObject(val: any): val is ICSSInJSStyle { + return val != null && typeof val === 'object' && Array.isArray(val) === false; +} diff --git a/packages/fluentui/react-northstar-emotion-renderer/test/__snapshots__/emotionRenderer-test.ts.snap b/packages/fluentui/react-northstar-emotion-renderer/test/__snapshots__/emotionRenderer-test.ts.snap new file mode 100644 index 00000000000000..4d0210da3c9944 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/test/__snapshots__/emotionRenderer-test.ts.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`emotionRenderer animations are not applied if animations are disabled 1`] = ``; + +exports[`emotionRenderer array returned by keyframe results in CSS fallback values 1`] = ` +.fui-m7zzi0 { + -webkit-animation-name: animation-avzz4x; + animation-name: animation-avzz4x; +} +@-webkit-keyframes animation-avzz4x { + 0% { + color: yellow; + } + 100% { + color: yellow; + } +} +@keyframes animation-avzz4x { + 0% { + color: yellow; + } + 100% { + color: yellow; + } +} + +`; + +exports[`emotionRenderer basic styles are rendered 1`] = ` +.fui-tokvmb { + color: red; +} + +`; + +exports[`emotionRenderer keyframe colors are rendered 1`] = ` +.fui-nb4c5h { + -webkit-animation-name: animation-xikfch; + animation-name: animation-xikfch; + -webkit-animation-duration: 5s; + animation-duration: 5s; +} +@-webkit-keyframes animation-xikfch { + from { + color: red; + } + to { + color: blue; + } +} +@keyframes animation-xikfch { + from { + color: red; + } + to { + color: blue; + } +} + +`; + +exports[`emotionRenderer marginLeft is rendered into marginRight due to RTL 1`] = ` +.rfui-1y6ic72 { + margin-right: 10px; +} + +`; + +exports[`emotionRenderer prefixes required styles 1`] = ` +.fui-fsb97h { + cursor: zoom-in; + display: flex; + -webkit-filter: blur(5px); + filter: blur(5px); + height: min-content; + position: sticky; + -webkit-transition: width 2s, height 2s, background-color 2s, transform 2s; + transition: width 2s, height 2s, background-color 2s, transform 2s; +} +.fui-fsb97h:hover { + background-image: image-set("cat.png" 1x, "cat-2x.png" 2x); + display: inline-flex; + height: fit-content; +} + +`; diff --git a/packages/fluentui/react-northstar-emotion-renderer/test/emotionRenderer-test.ts b/packages/fluentui/react-northstar-emotion-renderer/test/emotionRenderer-test.ts new file mode 100644 index 00000000000000..01dbe414c8a355 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/test/emotionRenderer-test.ts @@ -0,0 +1,144 @@ +import { createEmotionRenderer } from '@fluentui/react-northstar-emotion-renderer'; +import { RendererParam } from '@fluentui/react-northstar-styles-renderer'; +import { ICSSInJSStyle } from '@fluentui/styles'; +// @ts-ignore No typings :( +import * as prettier from 'prettier'; + +expect.addSnapshotSerializer({ + test(value) { + return value?.nodeName === '#document'; + }, + print(value: Document) { + function reduceRules(sheet: CSSStyleSheet | undefined) { + return Array.from(sheet?.cssRules || []).reduce((acc, rule) => { + return `${acc}${rule.cssText}`; + }, ''); + } + + const insertedCSS = Array.from(value.head.childNodes) + .map((node: HTMLStyleElement) => reduceRules((node?.sheet as unknown) as CSSStyleSheet)) + .join(';'); + + return prettier.format(insertedCSS, { parser: 'css' }); + }, +}); + +const defaultRendererParam: RendererParam = { + direction: 'ltr', + disableAnimations: false, + displayName: 'Test', + sanitizeCss: false, +}; + +describe('emotionRenderer', () => { + beforeEach(() => { + document.head.innerHTML = 'u'; + }); + + test('basic styles are rendered', () => { + createEmotionRenderer().renderRule({ color: 'red' }, defaultRendererParam); + expect(document).toMatchSnapshot(); + }); + + // TODO: find out an issue with snapshots + // + // test('CSS fallback values are rendered', () => { + // createEmotionRenderer().renderRule({ display: ['grid', 'ms-grid'] as any }, defaultRendererParam); + // expect(document).toMatchSnapshot(); + // }); + + test('keyframe colors are rendered', () => { + const styles: ICSSInJSStyle = { + animationName: { + keyframe: ({ fromColor, toColor }) => ({ + from: { + color: fromColor, + }, + to: { + color: toColor, + }, + }), + params: { + fromColor: 'red', + toColor: 'blue', + }, + }, + animationDuration: '5s', + }; + + createEmotionRenderer().renderRule(styles, defaultRendererParam); + expect(document).toMatchSnapshot(); + }); + + test('array returned by keyframe results in CSS fallback values', () => { + const styles: ICSSInJSStyle = { + animationName: { + keyframe: ({ steps }) => { + const obj = {}; + steps.forEach((step: string) => { + (obj as any)[step] = { color: ['blue', 'red', 'yellow'] }; + }); + return obj; + }, + params: { steps: ['0%', '100%'] }, + }, + }; + + createEmotionRenderer().renderRule(styles, defaultRendererParam); + expect(document).toMatchSnapshot(); + }); + + test('animations are not applied if animations are disabled', () => { + const styles: ICSSInJSStyle = { + animationName: { + keyframe: { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }, + }, + }; + + createEmotionRenderer().renderRule(styles, { ...defaultRendererParam, disableAnimations: true }); + expect(document).toMatchSnapshot(); + }); + + test('marginLeft is rendered into marginRight due to RTL', () => { + createEmotionRenderer().renderRule({ marginLeft: '10px' }, { ...defaultRendererParam, direction: 'rtl' }); + expect(document).toMatchSnapshot(); + }); + + // TODO: Find a way to fix no-flip :( + // + // test('marginLeft is rendered into marginLeft due to RTL with `noFlip`', () => { + // createEmotionRenderer().renderRule( + // { marginLeft: '10px /* @noflip */' }, + // { ...defaultRendererParam, direction: 'rtl' }, + // ); + // expect(document).toMatchSnapshot(); + // }); + + test('prefixes required styles', () => { + createEmotionRenderer().renderRule( + { + cursor: 'zoom-in', + display: 'flex', + filter: 'blur(5px)', + height: 'min-content', + position: 'sticky', + transition: 'width 2s, height 2s, background-color 2s, transform 2s', + + ':hover': { + backgroundImage: 'image-set("cat.png" 1x, "cat-2x.png" 2x)', + display: 'inline-flex', + height: 'fit-content', + }, + }, + defaultRendererParam, + ); + expect(document).toMatchSnapshot(); + }); +}); diff --git a/packages/fluentui/react-northstar-emotion-renderer/tsconfig.json b/packages/fluentui/react-northstar-emotion-renderer/tsconfig.json new file mode 100644 index 00000000000000..50d1e6a45ec130 --- /dev/null +++ b/packages/fluentui/react-northstar-emotion-renderer/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../scripts/typescript/tsconfig.fluentui", + "compilerOptions": { + "composite": true, + "outDir": "dist/dts", + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "noUnusedParameters": true, + "strictNullChecks": true + }, + "include": ["src", "test"], + "references": [ + { + "path": "../styles" + }, + { + "path": "../react-northstar-styles-renderer" + } + ] +} diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/Accordion/accordionTitleStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/components/Accordion/accordionTitleStyles.ts index 097507ef5bc887..8d12197f7c9dde 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/components/Accordion/accordionTitleStyles.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/components/Accordion/accordionTitleStyles.ts @@ -33,12 +33,12 @@ const accordionTitleStyles: ComponentSlotStylesPrepared ({ alignItems: 'center', display: 'grid', - '-ms-grid-column': '2', + msGridColumn: '2', }), }; diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/Alert/alertDismissActionStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/components/Alert/alertDismissActionStyles.ts index 29fd1c510511ac..9af19a87f21a8f 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/components/Alert/alertDismissActionStyles.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/components/Alert/alertDismissActionStyles.ts @@ -42,7 +42,7 @@ const alertDismissActionStyles: ComponentSlotStylesPrepared = { .join(' '), ...(vertical && { gridAutoFlow: 'row', - '-ms-grid-columns': '1fr', + msGridColumns: '1fr', }), }; }, @@ -51,21 +51,21 @@ const layoutStyles: ComponentSlotStylesPrepared = { ...(p.debug && debugArea()), alignItems: 'center', display: 'inline-flex', - [p.vertical ? '-ms-grid-row' : '-ms-grid-column']: '1', + [p.vertical ? 'msGridRow' : 'msGridColumn']: '1', }), main: ({ props: p }): ICSSInJSStyle => ({ ...(p.debug && debugArea()), alignItems: 'center', display: ['grid', '-ms-grid'], - [p.vertical ? '-ms-grid-row' : '-ms-grid-column']: countTrue([p.hasStart, p.hasStart && p.gap, p.hasMain]), + [p.vertical ? 'msGridRow' : 'msGridColumn']: countTrue([p.hasStart, p.hasStart && p.gap, p.hasMain]), }), end: ({ props: p }): ICSSInJSStyle => ({ ...(p.debug && debugArea()), alignItems: 'center', display: 'inline-flex', - [p.vertical ? '-ms-grid-row' : '-ms-grid-column']: countTrue([p.hasStart, p.hasStart && p.gap, p.hasMain && p.gap]), + [p.vertical ? 'msGridRow' : 'msGridColumn']: countTrue([p.hasStart, p.hasStart && p.gap, p.hasMain && p.gap]), }), }; diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/SvgIcon/svgIconStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/components/SvgIcon/svgIconStyles.ts index 4cd848e0caddf4..bc937ca471661e 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/components/SvgIcon/svgIconStyles.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/components/SvgIcon/svgIconStyles.ts @@ -83,7 +83,7 @@ const svgIconStyles: ComponentSlotStylesPrepared { + svg: ({ props: { size, disabled, rotate }, variables: v, rtl }): ICSSInJSStyle => { const iconSizeInRems = getIconSize(size, v); return { @@ -96,7 +96,11 @@ const svgIconStyles: ComponentSlotStylesPrepared