From ee3b017cac1181ceacf4ccefef8210fb51ee9000 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Mon, 15 Nov 2021 12:00:12 +0100 Subject: [PATCH 1/8] proto: use compat layer between makeStyles & Fela --- package.json | 11 +-- .../VariableResolver/VariableResolver.tsx | 9 +- .../VariableResolver/useEnhancedRenderer.ts | 15 +-- packages/fluentui/react-bindings/package.json | 1 + .../fluentui/react-bindings/src/context.ts | 14 ++- .../react-bindings/src/hooks/useStyles.ts | 5 +- .../react-bindings/src/styles/types.ts | 2 + .../package.json | 2 +- .../src/createFelaRenderer.tsx | 97 +++++++++---------- .../src/felaPerformanceEnhancer.ts | 10 +- .../src/felaStylisEnhancer.ts | 21 +--- .../src/makeStylesCompat/generateClassName.ts | 8 ++ .../makeStylesCompat/getStyleBucketName.ts | 67 +++++++++++++ .../getStyleSheetForBucket.ts | 61 ++++++++++++ .../src/makeStylesCompat/index.ts | 5 + .../src/makeStylesCompat/insertRule.ts | 93 ++++++++++++++++++ .../src/makeStylesCompat/types.ts | 36 +++++++ .../src/types.ts | 13 +++ .../Animation/useAnimationStyles.ts | 4 +- .../src/components/Design/Design.tsx | 5 +- .../src/components/Provider/Provider.tsx | 14 +-- .../src/utils/createComponentInternal.ts | 4 +- .../src/utils/mergeProviderContexts.ts | 34 +------ .../src/utils/renderComponent.tsx | 17 +++- 24 files changed, 401 insertions(+), 147 deletions(-) create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/generateClassName.ts create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleBucketName.ts create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleSheetForBucket.ts create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/index.ts create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/insertRule.ts create mode 100644 packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/types.ts diff --git a/package.json b/package.json index 4d853eefe82ea1..f5604c4afbb576 100644 --- a/package.json +++ b/package.json @@ -249,11 +249,6 @@ "typings" ], "nohoist": [ - "@fluentui/make-styles/@types/stylis", - "@fluentui/make-styles/stylis", - "@fluentui/react-northstar-fela-renderer/stylis", - "@fluentui/react-northstar-emotion-renderer/@types/stylis", - "@fluentui/react-northstar-emotion-renderer/stylis", "@fluentui/web-components/@microsoft/eslint-config-fast-dna", "@fluentui/web-components/@storybook/html", "@fluentui/web-components/ts-loader", @@ -370,8 +365,7 @@ "@fluentui/dom-utilities", "@fluentui/eslint-plugin", "@fluentui/react", - "@fluentui/react-conformance", - "stylis" + "@fluentui/react-conformance" ] }, { @@ -379,8 +373,7 @@ "@fluentui/react-northstar-emotion-renderer" ], "dependencies": [ - "@fluentui/eslint-plugin", - "stylis" + "@fluentui/eslint-plugin" ] }, { diff --git a/packages/fluentui/docs/src/components/VariableResolver/VariableResolver.tsx b/packages/fluentui/docs/src/components/VariableResolver/VariableResolver.tsx index 56d09788c4c102..012d5d21cf22f5 100644 --- a/packages/fluentui/docs/src/components/VariableResolver/VariableResolver.tsx +++ b/packages/fluentui/docs/src/components/VariableResolver/VariableResolver.tsx @@ -1,4 +1,4 @@ -import { useFluentContext, Unstable_FluentContextProvider } from '@fluentui/react-northstar'; +import { useFluentContext, useNorthstarRenderer, Unstable_NorthstarRendererContext } from '@fluentui/react-northstar'; import * as _ from 'lodash'; import * as React from 'react'; @@ -15,8 +15,9 @@ const VariableResolver: React.FunctionComponent = props = const elementRef = React.useRef(); const latestVariables = React.useRef({}); + const renderer = useNorthstarRenderer(); const context = useFluentContext(); - const [enhancedContext, resolvedVariables] = useEnhancedRenderer(context); + const [enhancedRenderer, resolvedVariables] = useEnhancedRenderer(context, renderer); const onClassNamesChange = React.useCallback(() => { if (!_.isEqual(resolvedVariables.current, latestVariables.current)) { @@ -32,9 +33,9 @@ const VariableResolver: React.FunctionComponent = props = useClassNamesListener(elementRef, onClassNamesChange); return ( - +
{props.children}
-
+ ); }; diff --git a/packages/fluentui/docs/src/components/VariableResolver/useEnhancedRenderer.ts b/packages/fluentui/docs/src/components/VariableResolver/useEnhancedRenderer.ts index c4a3e47394213d..41bf2e6869db52 100644 --- a/packages/fluentui/docs/src/components/VariableResolver/useEnhancedRenderer.ts +++ b/packages/fluentui/docs/src/components/VariableResolver/useEnhancedRenderer.ts @@ -9,7 +9,8 @@ export type UsedVariables = Record>; /** Enhances passed Fela or Emotion renderer to get actual variables. */ const useEnhancedRenderer = ( context: ProviderContextPrepared, -): [ProviderContextPrepared, React.RefObject] => { + renderer: Renderer, +): [Renderer, React.RefObject] => { const resolvedVariables = React.useRef({}); const renderRule: Renderer['renderRule'] = React.useCallback( (styles, rendererParam) => { @@ -26,20 +27,14 @@ const useEnhancedRenderer = ( } }); - return context.renderer.renderRule(styles, rendererParam); + return renderer.renderRule(styles, rendererParam); }, [context], ); - const enhancedContext: ProviderContextPrepared = React.useMemo( - () => ({ - ...context, - renderer: { ...context.renderer, renderRule }, - }), - [context, renderRule], - ); + const enhancedRenderer: Renderer = React.useMemo(() => ({ ...renderer, renderRule }), [context, renderRule]); - return [enhancedContext, resolvedVariables]; + return [enhancedRenderer, resolvedVariables]; }; export default useEnhancedRenderer; diff --git a/packages/fluentui/react-bindings/package.json b/packages/fluentui/react-bindings/package.json index 1e78894a711cb4..18605f7f8c4a49 100644 --- a/packages/fluentui/react-bindings/package.json +++ b/packages/fluentui/react-bindings/package.json @@ -7,6 +7,7 @@ "dependencies": { "@babel/runtime": "^7.10.4", "@fluentui/accessibility": "^0.59.0", + "@fluentui/make-styles": "9.0.0-beta.2", "@fluentui/react-component-event-listener": "^0.59.0", "@fluentui/react-component-ref": "^0.59.0", "@fluentui/react-northstar-fela-renderer": "^0.59.0", diff --git a/packages/fluentui/react-bindings/src/context.ts b/packages/fluentui/react-bindings/src/context.ts index 5d2f35bdfc3d26..255b78ba68f997 100644 --- a/packages/fluentui/react-bindings/src/context.ts +++ b/packages/fluentui/react-bindings/src/context.ts @@ -1,4 +1,6 @@ -import { noopRenderer, Renderer } from '@fluentui/react-northstar-styles-renderer'; +import { createDOMRenderer } from '@fluentui/make-styles'; +import { Renderer } from '@fluentui/react-northstar-styles-renderer'; +import { createFelaRenderer } from '@fluentui/react-northstar-fela-renderer'; import { emptyTheme, ThemeInput, ThemePrepared } from '@fluentui/styles'; import * as React from 'react'; @@ -27,7 +29,6 @@ export type ProviderContextPrepared = { rtl: boolean; disableAnimations: boolean; performance: StylesContextPerformance; - renderer: Renderer; theme: ThemePrepared; telemetry: Telemetry | undefined; // `target` can be undefined for SSR @@ -46,7 +47,6 @@ export const defaultContextValue: ProviderContextPrepared = { rtl: undefined as any, disableAnimations: false, performance: defaultPerformanceFlags, - renderer: noopRenderer, theme: emptyTheme, telemetry: undefined, target: undefined, @@ -59,3 +59,11 @@ export function useFluentContext(): ProviderContextPrepared { } export const Unstable_FluentContextProvider = FluentContext.Provider; + +export const Unstable_NorthstarRendererContext = React.createContext( + createFelaRenderer(document, createDOMRenderer(document)), +); + +export function useNorthstarRenderer(): Renderer { + return React.useContext(Unstable_NorthstarRendererContext); +} diff --git a/packages/fluentui/react-bindings/src/hooks/useStyles.ts b/packages/fluentui/react-bindings/src/hooks/useStyles.ts index 05817a9341387f..38c43f8e6fc586 100644 --- a/packages/fluentui/react-bindings/src/hooks/useStyles.ts +++ b/packages/fluentui/react-bindings/src/hooks/useStyles.ts @@ -2,7 +2,7 @@ import { ComposePreparedOptions } from '../compose'; import { ComponentSlotStyle, ComponentSlotStylesResolved, ComponentVariablesInput, DebugData } from '@fluentui/styles'; import * as React from 'react'; -import { useFluentContext } from '../context'; +import { useFluentContext, useNorthstarRenderer } from '../context'; import { ComponentDesignProp, ComponentSlotClasses, PrimitiveProps } from '../styles/types'; import { getStyles } from '../styles/getStyles'; @@ -57,6 +57,7 @@ export const useStyles = ( displayName: string, options: UseStylesOptions, ): UseStylesResult => { + const renderer = useNorthstarRenderer(); const context = useFluentContext(); const { @@ -91,7 +92,7 @@ export const useStyles = ( // Context values disableAnimations: context.disableAnimations, - renderer: context.renderer, + renderer, rtl, saveDebug: fluentUIDebug => (debug.current = { fluentUIDebug }), theme: context.theme, diff --git a/packages/fluentui/react-bindings/src/styles/types.ts b/packages/fluentui/react-bindings/src/styles/types.ts index bdc6a4ce4bd14c..7f528fcaa7e7fb 100644 --- a/packages/fluentui/react-bindings/src/styles/types.ts +++ b/packages/fluentui/react-bindings/src/styles/types.ts @@ -1,5 +1,6 @@ import { DebugData, ICSSInJSStyle, PropsWithVarsAndStyles } from '@fluentui/styles'; import { ProviderContextPrepared } from '../context'; +import { Renderer } from '@fluentui/react-northstar-styles-renderer'; // Notice: // This temporary lives here, will be remove once `animation` prop will be dropped @@ -57,5 +58,6 @@ export type ResolveStylesOptions = Omit & { componentProps: Record; inlineStylesProps: PropsWithVarsAndStyles & { design?: ComponentDesignProp }; rtl: boolean; + renderer: Renderer; saveDebug: (debug: DebugData | null) => void; }; diff --git a/packages/fluentui/react-northstar-fela-renderer/package.json b/packages/fluentui/react-northstar-fela-renderer/package.json index f91d9daef646db..bd9c6f79194e21 100644 --- a/packages/fluentui/react-northstar-fela-renderer/package.json +++ b/packages/fluentui/react-northstar-fela-renderer/package.json @@ -17,7 +17,7 @@ "inline-style-expand-shorthand": "^1.2.0", "lodash": "^4.17.15", "react-fela": "^10.6.1", - "stylis": "^3.5.4" + "stylis": "^4.0.6" }, "devDependencies": { "@fluentui/eslint-plugin": "*", diff --git a/packages/fluentui/react-northstar-fela-renderer/src/createFelaRenderer.tsx b/packages/fluentui/react-northstar-fela-renderer/src/createFelaRenderer.tsx index c8e0bbaf59ad84..484315794f25e1 100644 --- a/packages/fluentui/react-northstar-fela-renderer/src/createFelaRenderer.tsx +++ b/packages/fluentui/react-northstar-fela-renderer/src/createFelaRenderer.tsx @@ -1,5 +1,5 @@ -import { CreateRenderer } from '@fluentui/react-northstar-styles-renderer'; -import { createRenderer, IRenderer, IStyle, TPlugin } from 'fela'; +import { Renderer } from '@fluentui/react-northstar-styles-renderer'; +import { createRenderer, IConfig, IRenderer, IStyle, TPlugin } from 'fela'; import felaPluginEmbedded from 'fela-plugin-embedded'; import felaPluginFallbackValue from 'fela-plugin-fallback-value'; import felaPluginPlaceholderPrefixer from 'fela-plugin-placeholder-prefixer'; @@ -14,7 +14,9 @@ import { felaInvokeKeyframesPlugin } from './felaInvokeKeyframesPlugin'; import { felaPerformanceEnhancer } from './felaPerformanceEnhancer'; import { felaSanitizeCssPlugin } from './felaSanitizeCssPlugin'; import { felaStylisEnhancer } from './felaStylisEnhancer'; -import { FelaRendererParam } from './types'; +import { insertRule } from './makeStylesCompat'; +import { FelaRenderer, FelaRendererParam } from './types'; +import type { MakeStylesRenderer } from './makeStylesCompat'; let felaDevMode = false; @@ -46,67 +48,60 @@ if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { } } -const blocklistedClassNames = [ - // Blocklist contains a list of classNames that are used by FontAwesome - // https://fontawesome.com/how-to-use/on-the-web/referencing-icons/basic-use - 'fa', - 'fas', - 'far', - 'fal', - 'fab', - // Used by https://github.com/fullcalendar/fullcalendar - 'fc', - // .cke is used by CKEditor - 'ck', - 'cke', -]; - -const filterClassName = (className: string): boolean => { - // Also ensure that class name does not contain 'ad' as it might - // cause compatibility issues regarding Ad blockers. - return className.indexOf('ad') === -1 && blocklistedClassNames.indexOf(className) === -1; +const filterClassName = (): boolean => { + return true; }; -const rendererConfig = { - devMode: felaDevMode, - filterClassName, - enhancers: [felaPerformanceEnhancer, felaFocusVisibleEnhancer, felaStylisEnhancer], - plugins: [ - felaDisableAnimationsPlugin as TPlugin, +type CreateFelaRenderer = (target?: Document, makeStylesRenderer?: MakeStylesRenderer) => Renderer; - // is necessary to prevent accidental style typos - // from breaking ALL the styles on the page - felaSanitizeCssPlugin as TPlugin, +export const createFelaRenderer: CreateFelaRenderer = (target, makeStylesRenderer) => { + const rendererConfig: IConfig = { + devMode: felaDevMode, + filterClassName, + enhancers: [felaPerformanceEnhancer, felaFocusVisibleEnhancer, felaStylisEnhancer], + plugins: [ + felaDisableAnimationsPlugin as TPlugin, - felaPluginPlaceholderPrefixer(), - felaInvokeKeyframesPlugin as TPlugin, - felaPluginEmbedded(), + // is necessary to prevent accidental style typos + // from breaking ALL the styles on the page + felaSanitizeCssPlugin as TPlugin, - felaExpandCssShorthandsPlugin as TPlugin, + felaPluginPlaceholderPrefixer(), + felaInvokeKeyframesPlugin as TPlugin, + felaPluginEmbedded(), - // Heads up! - // This is required after fela-plugin-prefixer to resolve the array of fallback values prefixer produces. - felaPluginFallbackValue(), + felaExpandCssShorthandsPlugin as TPlugin, - felaPluginRtl(), - ], -}; + // Heads up! + // This is required after fela-plugin-prefixer to resolve the array of fallback values prefixer produces. + felaPluginFallbackValue(), -export const createFelaRenderer: CreateRenderer = target => { - const felaRenderer = createRenderer(rendererConfig) as IRenderer & { - listeners: []; - nodes: Record; - updateSubscription: Function | undefined; + felaPluginRtl(), + ], }; + const felaRenderer = createRenderer(rendererConfig) as FelaRenderer; + + felaRenderer.isCompat = !!makeStylesRenderer; let usedRenderers: number = 0; // rehydration disabled to avoid leaking styles between renderers // https://github.com/rofrischmann/fela/blob/master/docs/api/fela-dom/rehydrate.md - const Provider: React.FC = props => ( - - {props.children} - - ); + const Provider: React.FC = props => { + if (felaRenderer.isCompat) { + if (!felaRenderer.updateSubscription) { + felaRenderer.updateSubscription = insertRule(target, makeStylesRenderer!); + felaRenderer.subscribe(felaRenderer.updateSubscription); + } + + return <>{props.children}; + } + + return ( + + {props.children} + + ); + }; return { registerUsage: () => { diff --git a/packages/fluentui/react-northstar-fela-renderer/src/felaPerformanceEnhancer.ts b/packages/fluentui/react-northstar-fela-renderer/src/felaPerformanceEnhancer.ts index c4ec7bf3ecc4d2..1214a34ecf41c9 100644 --- a/packages/fluentui/react-northstar-fela-renderer/src/felaPerformanceEnhancer.ts +++ b/packages/fluentui/react-northstar-fela-renderer/src/felaPerformanceEnhancer.ts @@ -25,6 +25,7 @@ import { RULE_TYPE, } from 'fela-utils'; +import { generateClassName as generateCompatClassName, getStyleBucketName } from './makeStylesCompat'; import { FelaRenderer, FelaRendererChange } from './types'; function isPlainObject(val: any) { @@ -120,13 +121,15 @@ Check http://fela.js.org/docs/basics/Rules.html#styleobject for more information /* eslint-enable */ } - const className = - renderer.selectorPrefix + generateClassName(renderer.getNextRuleIdentifier, renderer.filterClassName); + const atomicClassName = renderer.isCompat + ? generateCompatClassName(property, value, pseudo, media, support) + : generateClassName(renderer.getNextRuleIdentifier, renderer.filterClassName); + const className = renderer.selectorPrefix + atomicClassName; const declaration = cssifyDeclaration(property, value); const selector = generateCSSSelector(className, pseudo); - const change = { + const change: FelaRendererChange = { type: RULE_TYPE, className, selector, @@ -134,6 +137,7 @@ Check http://fela.js.org/docs/basics/Rules.html#styleobject for more information pseudo, media, support, + bucket: getStyleBucketName(pseudo, media, support), }; renderer.cache[declarationReference] = change; diff --git a/packages/fluentui/react-northstar-fela-renderer/src/felaStylisEnhancer.ts b/packages/fluentui/react-northstar-fela-renderer/src/felaStylisEnhancer.ts index e9b842afd92353..30c0b9880b63b2 100644 --- a/packages/fluentui/react-northstar-fela-renderer/src/felaStylisEnhancer.ts +++ b/packages/fluentui/react-northstar-fela-renderer/src/felaStylisEnhancer.ts @@ -1,32 +1,17 @@ import { RULE_TYPE } from 'fela-utils'; -// @ts-ignore -import _Stylis from 'stylis'; +import { compile, middleware, serialize, stringify, prefixer } from 'stylis'; import { FelaRenderer, FelaRendererChange } from './types'; -// `stylis@3` is a CJS library, there are known issues with them: -// https://github.com/rollup/rollup/issues/1267#issuecomment-446681320 -const Stylis = (_Stylis as any).default || _Stylis; - -// We use Stylis only for vendor prefixing, all other capabilities are disabled -const stylis = new Stylis({ - cascade: false, - compress: false, - global: false, - keyframe: false, - preserve: false, - semicolon: false, -}); - export const felaStylisEnhancer = (renderer: FelaRenderer) => { const existingEmitChange = renderer._emitChange.bind(renderer); renderer._emitChange = (change: FelaRendererChange) => { if (change.type === RULE_TYPE) { - const prefixed: string = stylis('', change.declaration); + const prefixed: string = serialize(compile(change.declaration), middleware([prefixer, stringify])); // Fela uses objects by references, it's safe to override properties - change.declaration = prefixed.slice(1, -1); + change.declaration = prefixed.slice(0, -1); } existingEmitChange(change); diff --git a/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/generateClassName.ts b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/generateClassName.ts new file mode 100644 index 00000000000000..79c2ab5591e35a --- /dev/null +++ b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/generateClassName.ts @@ -0,0 +1,8 @@ +import hashString from '@emotion/hash'; + +const HASH_PREFIX = 'f'; + +export function generateClassName(property: string, value: any, pseudo?: string, media?: string, support?: string) { + // Trimming of value is required to generate consistent hashes + return HASH_PREFIX + hashString(pseudo! + media! + support + property + value.toString().trim()); +} diff --git a/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleBucketName.ts b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleBucketName.ts new file mode 100644 index 00000000000000..50569bedf03832 --- /dev/null +++ b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleBucketName.ts @@ -0,0 +1,67 @@ +import { StyleBucketName } from './types'; + +/** + * Maps the long pseudo name to the short pseudo name. Pseudos that match here will be ordered, everything else will + * make their way to default style bucket. We reduce the pseudo name to save bundlesize. + * Thankfully there aren't any overlaps, see: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes. + */ +const pseudosMap: Record = { + // :focus-within + 'us-w': 'w', + // :focus-visible + 'us-v': 'i', + + // :link + nk: 'l', + // :visited + si: 'v', + // :focus + cu: 'f', + // :hover + ve: 'h', + // :active + ti: 'a', +}; + +/** + * Gets the bucket depending on the pseudo. + * + * Input: + * + * ``` + * ":hover" + * ":focus:hover" + * ``` + * + * Output: + * + * ``` + * "h" + * "f" + * ``` + */ +export function getStyleBucketName(pseudo: string, media: string, support: string): StyleBucketName { + // We are grouping all the at-rules like @media, @supports etc under `t` bucket. + if (media || support) { + return 't'; + } + + const normalizedPseudo = pseudo.trim(); + + if (normalizedPseudo.charCodeAt(0) === 58 /* ":" */) { + // We send through a subset of the string instead of the full pseudo name. + // For example: + // - `"focus-visible"` name would instead of `"us-v"`. + // - `"focus"` name would instead of `"us"`. + // Return a mapped pseudo else default bucket. + + return ( + pseudosMap[normalizedPseudo.slice(4, 8)] /* allows to avoid collisions between "focus-visible" & "focus" */ || + pseudosMap[normalizedPseudo.slice(3, 5)] || + 'd' + ); + } + + // Return default bucket + return 'd'; +} diff --git a/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleSheetForBucket.ts b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleSheetForBucket.ts new file mode 100644 index 00000000000000..df2b0a1c4f851d --- /dev/null +++ b/packages/fluentui/react-northstar-fela-renderer/src/makeStylesCompat/getStyleSheetForBucket.ts @@ -0,0 +1,61 @@ +import { MakeStylesRenderer, StyleBucketName } from './types'; + +/** + * Ordered style buckets using their short pseudo name. + * + * @private + */ +export const styleBucketOrdering: StyleBucketName[] = [ + // catch-all + 'd', + // link + 'l', + // visited + 'v', + // focus-within + 'w', + // focus + 'f', + // focus-visible + 'i', + // hover + 'h', + // active + 'a', + // keyframes + 'k', + // at-rules + 't', +]; + +/** + * Lazily adds a `