diff --git a/change/@fluentui-make-styles-ad78d37d-58a7-46c5-83a2-cac7cce8fcff.json b/change/@fluentui-make-styles-ad78d37d-58a7-46c5-83a2-cac7cce8fcff.json new file mode 100644 index 0000000000000..6ba782ffc2cc9 --- /dev/null +++ b/change/@fluentui-make-styles-ad78d37d-58a7-46c5-83a2-cac7cce8fcff.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix insertion of keyframes", + "packageName": "@fluentui/make-styles", + "email": "olfedias@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-make-styles-0c89fe8b-4478-430d-a761-fc6ead7c7eae.json b/change/@fluentui-react-make-styles-0c89fe8b-4478-430d-a761-fc6ead7c7eae.json new file mode 100644 index 0000000000000..be8bda64fad18 --- /dev/null +++ b/change/@fluentui-react-make-styles-0c89fe8b-4478-430d-a761-fc6ead7c7eae.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "add section about keyframes to README", + "packageName": "@fluentui/react-make-styles", + "email": "olfedias@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/make-styles/src/makeStyles.test.ts b/packages/make-styles/src/makeStyles.test.ts index 4b89e34cceae0..082e2bb97efae 100644 --- a/packages/make-styles/src/makeStyles.test.ts +++ b/packages/make-styles/src/makeStyles.test.ts @@ -129,6 +129,34 @@ describe('makeStyles', () => { transform: rotate(-360deg); } } + @keyframes f1q8eu9e { + from { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } + } + @keyframes f55c0se { + from { + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); + } + to { + -webkit-transform: rotate(-360deg); + -moz-transform: rotate(-360deg); + -ms-transform: rotate(-360deg); + transform: rotate(-360deg); + } + } .f1g6ul6r { -webkit-animation-name: f1q8eu9e; animation-name: f1q8eu9e; diff --git a/packages/make-styles/src/renderer/rehydrateRendererCache.ts b/packages/make-styles/src/renderer/rehydrateRendererCache.ts index 90472f9bc7f55..ffc3edcbd1a30 100644 --- a/packages/make-styles/src/renderer/rehydrateRendererCache.ts +++ b/packages/make-styles/src/renderer/rehydrateRendererCache.ts @@ -3,7 +3,7 @@ import { MakeStylesRenderer, StyleBucketName } from '../types'; // Regexps to extract names of classes and animations // https://github.com/styletron/styletron/blob/e0fcae826744eb00ce679ac613a1b10d44256660/packages/styletron-engine-atomic/src/client/client.js#L8 // eslint-disable-next-line @fluentui/max-len -const KEYFRAMES_HYDRATOR = /@-webkit-keyframes ([^{]+){((?:(?:from|to|(?:\d+\.?\d*%))\{(?:[^}])*})*)}@keyframes ([^{]+){((?:(?:from|to|(?:\d+\.?\d*%))\{(?:[^}])*})*)}/g; +const KEYFRAMES_HYDRATOR = /@(-webkit-)?keyframes ([^{]+){((?:(?:from|to|(?:\d+\.?\d*%))\{(?:[^}])*})*)}/g; const AT_RULES_HYDRATOR = /@(media|supports)[^{]+\{([\s\S]+?})\s*}/g; const STYLES_HYDRATOR = /\.([^{:]+)(:[^{]+)?{(?:[^}]*;)?([^}]*?)}/g; diff --git a/packages/make-styles/src/runtime/compileKeyframeCSS.test.ts b/packages/make-styles/src/runtime/compileKeyframeCSS.test.ts new file mode 100644 index 0000000000000..812ff1c36b43e --- /dev/null +++ b/packages/make-styles/src/runtime/compileKeyframeCSS.test.ts @@ -0,0 +1,40 @@ +import { compileKeyframeRule, compileKeyframesCSS } from './compileKeyframeCSS'; +import { MakeStyles } from '../types'; + +describe('compileKeyframeRule', () => { + it('stringifies an object with keyframes', () => { + const keyframes: MakeStyles = { + from: { + transform: 'rotate(0deg)', + }, + to: { + transform: 'rotate(360deg)', + }, + }; + const result = compileKeyframeRule(keyframes); + + expect(result).toMatchInlineSnapshot(`"from{transform:rotate(0deg);}to{transform:rotate(360deg);}"`); + }); +}); + +describe('compileKeyframeCSS', () => { + it('creates CSS from strings with keyframes', () => { + const keyframes: MakeStyles = { + from: { + height: '10px', + }, + to: { + height: '50px', + }, + }; + const keyframesCSS = compileKeyframeRule(keyframes); + const result = compileKeyframesCSS('foo', keyframesCSS); + + expect(result).toMatchInlineSnapshot(` + Array [ + "@-webkit-keyframes foo{from{height:10px;}to{height:50px;}}", + "@keyframes foo{from{height:10px;}to{height:50px;}}", + ] + `); + }); +}); diff --git a/packages/make-styles/src/runtime/compileKeyframeCSS.ts b/packages/make-styles/src/runtime/compileKeyframeCSS.ts index b8139303ba3f2..cc93f319c371a 100644 --- a/packages/make-styles/src/runtime/compileKeyframeCSS.ts +++ b/packages/make-styles/src/runtime/compileKeyframeCSS.ts @@ -1,19 +1,37 @@ import { MakeStyles } from '../types'; -import { compile, middleware, serialize, stringify, prefixer } from 'stylis'; +import { compile, middleware, serialize, rulesheet, stringify, prefixer } from 'stylis'; import { cssifyObject } from './utils/cssifyObject'; -export function compileKeyframeRule(frames: MakeStyles): string { +export function compileKeyframeRule(keyframeObject: MakeStyles): string { let css: string = ''; // eslint-disable-next-line guard-for-in - for (const percentage in frames) { - css += `${percentage}{${cssifyObject(frames[percentage])}}`; + for (const percentage in keyframeObject) { + css += `${percentage}{${cssifyObject(keyframeObject[percentage])}}`; } return css; } -export function compileKeyframesCSS(animationName: string, framesCSS: string): string { - const cssRule = `@keyframes ${animationName} {${framesCSS}}`; - return serialize(compile(cssRule), middleware([prefixer, stringify])); +/** + * Creates CSS rules for insertion from passed CSS. + */ +export function compileKeyframesCSS(keyframeName: string, keyframeCSS: string): string[] { + const cssRule = `@keyframes ${keyframeName} {${keyframeCSS}}`; + const rules: string[] = []; + + serialize( + compile(cssRule), + middleware([ + prefixer, + stringify, + + // 💡 we are using `.insertRule()` API for DOM operations, which does not support + // insertion of multiple CSS rules in a single call. `rulesheet` plugin extracts + // individual rules to be used with this API + rulesheet(rule => rules.push(rule)), + ]), + ); + + return rules; } diff --git a/packages/make-styles/src/runtime/resolveStyleRules.test.ts b/packages/make-styles/src/runtime/resolveStyleRules.test.ts index 266b012420f1e..d26769f702d7f 100644 --- a/packages/make-styles/src/runtime/resolveStyleRules.test.ts +++ b/packages/make-styles/src/runtime/resolveStyleRules.test.ts @@ -524,7 +524,7 @@ describe('resolveStyleRules', () => { transform: rotate(360deg); } } - @keyframes f1q8eu9e { + @-webkit-keyframes f55c0se { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -532,13 +532,13 @@ describe('resolveStyleRules', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - transform: rotate(360deg); + -webkit-transform: rotate(-360deg); + -moz-transform: rotate(-360deg); + -ms-transform: rotate(-360deg); + transform: rotate(-360deg); } } - @-webkit-keyframes f55c0se { + @keyframes f1q8eu9e { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -546,10 +546,10 @@ describe('resolveStyleRules', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(-360deg); - -moz-transform: rotate(-360deg); - -ms-transform: rotate(-360deg); - transform: rotate(-360deg); + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); } } @keyframes f55c0se { @@ -624,7 +624,7 @@ describe('resolveStyleRules', () => { transform: rotate(360deg); } } - @keyframes f1q8eu9e { + @-webkit-keyframes f55c0se { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -632,29 +632,13 @@ describe('resolveStyleRules', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - transform: rotate(360deg); - } - } - @-webkit-keyframes f5j8bii { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - @keyframes f5j8bii { - from { - opacity: 0; - } - to { - opacity: 1; + -webkit-transform: rotate(-360deg); + -moz-transform: rotate(-360deg); + -ms-transform: rotate(-360deg); + transform: rotate(-360deg); } } - @-webkit-keyframes f55c0se { + @keyframes f1q8eu9e { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -662,10 +646,10 @@ describe('resolveStyleRules', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(-360deg); - -moz-transform: rotate(-360deg); - -ms-transform: rotate(-360deg); - transform: rotate(-360deg); + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); } } @keyframes f55c0se { @@ -682,13 +666,29 @@ describe('resolveStyleRules', () => { transform: rotate(-360deg); } } - .f1al5ov7 { - -webkit-animation-name: f1q8eu9e f5j8bii; - animation-name: f1q8eu9e f5j8bii; + @-webkit-keyframes f5j8bii { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes f5j8bii { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + .fng7zue { + -webkit-animation-name: f1q8eu9e, f5j8bii; + animation-name: f1q8eu9e, f5j8bii; } - .f1yfduy3 { - -webkit-animation-name: f55c0se f5j8bii; - animation-name: f55c0se f5j8bii; + .f12eevt1 { + -webkit-animation-name: f55c0se, f5j8bii; + animation-name: f55c0se, f5j8bii; } .f1cpbl36 { -webkit-animation-iteration-count: infinite; diff --git a/packages/make-styles/src/runtime/resolveStyleRules.ts b/packages/make-styles/src/runtime/resolveStyleRules.ts index 002606d8d1d3f..1a7ddb6796e7c 100644 --- a/packages/make-styles/src/runtime/resolveStyleRules.ts +++ b/packages/make-styles/src/runtime/resolveStyleRules.ts @@ -107,49 +107,52 @@ function resolveStyleRulesInner( pushToClassesMap(cssClassesMap, key, className, rtlClassName); pushToCSSRules(cssRulesByBucket, styleBucketName, ltrCSS, rtlCSS); } else if (property === 'animationName') { - const animationNames = Array.isArray(value) ? value : [value]; - let keyframeCSS = ''; - let keyframeRtlCSS = ''; + const animationNameValue = Array.isArray(value) ? value : [value]; - const names = []; - const namesRtl = []; + const animationNames: string[] = []; + const rtlAnimationNames: string[] = []; - for (const val of animationNames) { - const keyframe = compileKeyframeRule(val); - const name = HASH_PREFIX + hashString(keyframe); + for (const keyframeObject of animationNameValue) { + const keyframeCSS = compileKeyframeRule(keyframeObject); + const rtlKeyframeCSS = compileKeyframeRule(convert(keyframeObject)); - keyframeCSS += compileKeyframesCSS(name, keyframe); - names.push(name); + const animationName = HASH_PREFIX + hashString(keyframeCSS); + let rtlAnimationName: string; - const rtlKeyframe = compileKeyframeRule(convert(val)); + const keyframeRules = compileKeyframesCSS(animationName, keyframeCSS); + let rtlKeyframeRules: string[] = []; - if (keyframe !== rtlKeyframe) { - const nameRtl = HASH_PREFIX + hashString(rtlKeyframe); - keyframeRtlCSS += compileKeyframesCSS(nameRtl, rtlKeyframe); - namesRtl.push(nameRtl); + if (keyframeCSS === rtlKeyframeCSS) { + // If CSS for LTR & RTL are same we will re-use animationName from LTR to avoid duplication of rules in output + rtlAnimationName = animationName; } else { - namesRtl.push(name); + rtlAnimationName = HASH_PREFIX + hashString(rtlKeyframeCSS); + rtlKeyframeRules = compileKeyframesCSS(rtlAnimationName, rtlKeyframeCSS); } - } - const animationName = names.join(' '); - const animationNameRtl = namesRtl.join(' '); + for (let i = 0; i < keyframeRules.length; i++) { + pushToCSSRules( + cssRulesByBucket, + // keyframes styles should be inserted into own bucket + 'k', + keyframeRules[i], + rtlKeyframeRules[i], + ); + } + + animationNames.push(animationName); + rtlAnimationNames.push(rtlAnimationName); + } - pushToCSSRules( - cssRulesByBucket, - 'k', // keyframes styles should be inserted into own bucket - keyframeCSS, - keyframeRtlCSS || undefined, - ); resolveStyleRulesInner( - { animationName }, + { animationName: animationNames.join(', ') }, unstable_cssPriority, pseudo, media, support, cssClassesMap, cssRulesByBucket, - animationNameRtl, + rtlAnimationNames.join(', '), ); } else if (isObject(value)) { if (isNestedSelector(property)) { diff --git a/packages/react-make-styles/README.md b/packages/react-make-styles/README.md index 33b31b3fadfc8..1af77bfd0aa70 100644 --- a/packages/react-make-styles/README.md +++ b/packages/react-make-styles/README.md @@ -129,6 +129,39 @@ const useStyles = makeStyles({ }); ``` +### 🎞 `keyframes` (animations) + +`keyframes` are supported via `animationName` property that can be defined as an object or an array of objects: + +```tsx +import { makeStyles } from '@fluentui/react-components'; + +const useStyles = makeStyles({ + root: { + animationIterationCount: 'infinite', + animationDuration: '3s', + animationName: { + from: { transform: 'rotate(0deg)' }, + to: { transform: 'rotate(360deg)' }, + }, + }, + array: { + animationIterationCount: 'infinite', + animationDuration: '3s', + animationName: [ + { + from: { transform: 'rotate(0deg)' }, + to: { transform: 'rotate(360deg)' }, + }, + { + from: { height: '100px' }, + to: { height: '200px' }, + }, + ], + }, +}); +``` + ## `makeStaticStyles()` Creates styles attached to a global selector. Styles can be defined via objects: diff --git a/packages/react-make-styles/src/createDOMRenderer.test.tsx b/packages/react-make-styles/src/createDOMRenderer.test.tsx index b5e7e7ae1ce59..8a6c63f0af375 100644 --- a/packages/react-make-styles/src/createDOMRenderer.test.tsx +++ b/packages/react-make-styles/src/createDOMRenderer.test.tsx @@ -20,8 +20,8 @@ describe('createDOMRenderer', () => { const useExampleStyles = makeStyles({ root: { animationName: { - from: { transform: 'rotate(0deg)' }, - to: { transform: 'rotate(360deg)' }, + from: { height: '10px' }, + to: { height: '20px' }, }, color: 'red', @@ -80,6 +80,12 @@ describe('createDOMRenderer', () => { // We also would to ensure that new elements have not been inserted expect(styleElementsBeforeHydration.length).toBe(styleElementsAfterHydration.length); + // Following rules are present in cache: + // - "animationName" + // - "color" + // - @keyframes + prefixed + // - @media + expect(Object.keys(clientRenderer.insertionCache)).toHaveLength(5); insertRules.forEach(insertRule => { expect(insertRule).not.toHaveBeenCalled(); }); diff --git a/packages/react-make-styles/src/renderToStyleElements-node.test.tsx b/packages/react-make-styles/src/renderToStyleElements-node.test.tsx index 4ca1b7f344da7..66ed055821a95 100644 --- a/packages/react-make-styles/src/renderToStyleElements-node.test.tsx +++ b/packages/react-make-styles/src/renderToStyleElements-node.test.tsx @@ -142,7 +142,7 @@ describe('renderToStyleElements', () => { transform: rotate(360deg); } } - @keyframes f1q8eu9e { + @-webkit-keyframes f55c0se { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -150,13 +150,13 @@ describe('renderToStyleElements', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - transform: rotate(360deg); + -webkit-transform: rotate(-360deg); + -moz-transform: rotate(-360deg); + -ms-transform: rotate(-360deg); + transform: rotate(-360deg); } } - @-webkit-keyframes f55c0se { + @keyframes f1q8eu9e { from { -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); @@ -164,10 +164,10 @@ describe('renderToStyleElements', () => { transform: rotate(0deg); } to { - -webkit-transform: rotate(-360deg); - -moz-transform: rotate(-360deg); - -ms-transform: rotate(-360deg); - transform: rotate(-360deg); + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); } } @keyframes f55c0se {