import * as _ from 'lodash'; import { ComponentWithAs, getElementType, useUnhandledProps, StylesContextPerformanceInput, RendererContext, ProviderContextInput, ProviderContextPrepared, Telemetry, useFluentContext, unstable_getStyles, useIsomorphicLayoutEffect, Unstable_FluentContextProvider, } from '@fluentui/react-bindings'; import { Renderer } from '@fluentui/react-northstar-styles-renderer'; import { mergeSiteVariables, StaticStyleObject, StaticStyle, StaticStyleFunction, FontFace, ThemeInput, SiteVariablesPrepared, } from '@fluentui/styles'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { ChildrenComponentProps, setUpWhatInput, tryCleanupWhatInput, UIComponentProps } from '../../utils'; import { mergeProviderContexts } from '../../utils/mergeProviderContexts'; import { ProviderConsumer } from './ProviderConsumer'; import { usePortalBox, PortalBoxContext } from './usePortalBox'; export interface ProviderProps extends ChildrenComponentProps, UIComponentProps { rtl?: boolean; disableAnimations?: boolean; performance?: StylesContextPerformanceInput; overwrite?: boolean; target?: Document; theme?: ThemeInput; telemetryRef?: React.MutableRefObject; } const renderFontFaces = (renderer: Renderer, theme: ThemeInput) => { if (!theme.fontFaces) { return; } const renderFontObject = (font: FontFace) => { if (!_.isPlainObject(font)) { throw new Error(`fontFaces must be objects, got: ${typeof font}`); } renderer.renderFont(font); }; theme.fontFaces.forEach((font: FontFace) => { renderFontObject(font); }); }; const renderStaticStyles = (renderer: Renderer, theme: ThemeInput, siteVariables: SiteVariablesPrepared) => { if (!theme.staticStyles) { return; } const renderObject = (object: StaticStyleObject) => { _.forEach(object, (style, selector) => { renderer.renderGlobal(style, selector); }); }; theme.staticStyles.forEach((staticStyle: StaticStyle) => { if (typeof staticStyle === 'string') { renderer.renderGlobal(staticStyle); } else if (_.isPlainObject(staticStyle)) { renderObject(staticStyle as StaticStyleObject); } else if (_.isFunction(staticStyle)) { const preparedSiteVariables = mergeSiteVariables(siteVariables); renderObject((staticStyle as StaticStyleFunction)(preparedSiteVariables)); } else { throw new Error( `staticStyles array must contain CSS strings, style objects, or style functions, got: ${typeof staticStyle}`, ); } }); }; export const providerClassName = 'ui-provider'; /** * The Provider passes the CSS-in-JS renderer, theme styles and other settings to Fluent UI components. */ export const Provider: ComponentWithAs<'div', ProviderProps> & { Consumer: typeof ProviderConsumer; handledProps: (keyof ProviderProps)[]; } = props => { const { children, className, design, overwrite, styles, variables, telemetryRef } = props; const ElementType = getElementType(props); const unhandledProps = useUnhandledProps(Provider.handledProps, props); const telemetry = React.useMemo(() => { if (!telemetryRef) { return undefined; } if (!telemetryRef.current) { telemetryRef.current = new Telemetry(); } return telemetryRef.current; }, [telemetryRef]); const inputContext: ProviderContextInput = { disableAnimations: props.disableAnimations, performance: props.performance, rtl: props.rtl, target: props.target, telemetry, theme: props.theme, }; const consumedContext = useFluentContext(); const incomingContext: ProviderContextInput | ProviderContextPrepared = overwrite ? {} : consumedContext; const createRenderer = React.useContext(RendererContext); const outgoingContext = mergeProviderContexts(createRenderer, incomingContext, inputContext); const rtlProps: { dir?: 'rtl' | 'ltr' } = {}; // only add dir attribute for top level provider or when direction changes from parent to child if (consumedContext.rtl !== outgoingContext.rtl && _.isBoolean(outgoingContext.rtl)) { rtlProps.dir = outgoingContext.rtl ? 'rtl' : 'ltr'; } const { classes } = unstable_getStyles({ allDisplayNames: [Provider.displayName], className: providerClassName, primaryDisplayName: Provider.displayName, componentProps: {}, inlineStylesProps: { className, design, styles, variables, }, disableAnimations: outgoingContext.disableAnimations, performance: outgoingContext.performance, renderer: outgoingContext.renderer, rtl: outgoingContext.rtl, theme: outgoingContext.theme, saveDebug: _.noop, telemetry: undefined, }); const element = usePortalBox({ className: classes.root, target: outgoingContext.target, rtl: outgoingContext.rtl, }); useIsomorphicLayoutEffect(() => { renderFontFaces(outgoingContext.renderer, props.theme); renderStaticStyles(outgoingContext.renderer, props.theme, outgoingContext.theme.siteVariables); if (props.target) { setUpWhatInput(props.target); } return () => { if (props.target) { tryCleanupWhatInput(props.target); } }; }, []); // do not spread anything - React.Fragment can only have `key` and `children` props const elementProps = ElementType === React.Fragment ? {} : { className: classes.root, ...rtlProps, ...unhandledProps, }; const RenderProvider = outgoingContext.renderer.Provider; return ( {children} ); }; Provider.displayName = 'Provider'; Provider.defaultProps = { theme: {}, }; Provider.propTypes = { as: PropTypes.elementType, design: PropTypes.object, variables: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), styles: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), theme: PropTypes.shape({ siteVariables: PropTypes.object, componentVariables: PropTypes.object, componentStyles: PropTypes.object, fontFaces: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string, paths: PropTypes.arrayOf(PropTypes.string), style: PropTypes.shape({ fontStretch: PropTypes.string, fontStyle: PropTypes.string, fontVariant: PropTypes.string, fontWeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), localAlias: PropTypes.string, unicodeRange: PropTypes.string, }), }), ), staticStyles: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.func])), animations: PropTypes.object, }), rtl: PropTypes.bool, disableAnimations: PropTypes.bool, // Heads Up! // Keep in sync with packages/react-bindings/src/styles/types.ts performance: PropTypes.shape({ enableSanitizeCssPlugin: PropTypes.bool, enableStylesCaching: PropTypes.bool, enableVariablesCaching: PropTypes.bool, }), children: PropTypes.node.isRequired, overwrite: PropTypes.bool, target: PropTypes.object as PropTypes.Validator, telemetryRef: PropTypes.object as PropTypes.Validator>, }; Provider.handledProps = Object.keys(Provider.propTypes) as any; Provider.Consumer = ProviderConsumer;