import { mergeStyles } from '@fluentui/styles'; import { ComponentWithAs, ComposePreparedOptions, ShorthandConfig } from '@fluentui/react-bindings'; import cx from 'classnames'; import * as _ from 'lodash'; import * as React from 'react'; import * as ReactIs from 'react-is'; import { ShorthandValue, Props, PropsOf, ShorthandRenderFunction } from '../types'; type HTMLTag = 'iframe' | 'img' | 'input'; type ShorthandProp = 'children' | 'src' | 'type'; interface CreateShorthandOptions

{ /** Default props object */ defaultProps?: () => Partial>; /** Override props object or function (called with regular props) */ overrideProps?: Partial> | ((props: P) => Partial>); /** Whether or not automatic key generation is allowed */ generateKey?: boolean; /** Override the default render implementation. */ render?: ShorthandRenderFunction

; } // It's only necessary to map props that don't use 'children' as value ('children' is the default) const mappedProps: { [key in HTMLTag]: ShorthandProp } = { iframe: 'src', img: 'src', input: 'type', }; export type ShorthandFactory

= ( value: ShorthandValue

, options?: CreateShorthandOptions

, ) => React.ReactElement | null | undefined; // ============================================================ // Factory Creators // ============================================================ /** * @param config - Options passed to factory * @returns A shorthand factory function waiting for `val` and `defaultProps`. */ export function createShorthandFactory(config: { /** A ReactClass or string */ Component: TStringElement; /** A function that maps a primitive value to the Component props */ mappedProp?: keyof PropsOf; /** A function that maps an array value to the Component props */ mappedArrayProp?: keyof PropsOf; /** Indicates if factory supports React Elements */ allowsJSX?: boolean; }): ShorthandFactory

; export function createShorthandFactory(config: { Component: TFunctionComponent; mappedProp?: keyof PropsOf; mappedArrayProp?: keyof PropsOf; allowsJSX?: boolean; }): ShorthandFactory

; export function createShorthandFactory(config: { Component: { new (...args: any[]): TInstance }; mappedProp?: keyof PropsOf; mappedArrayProp?: keyof PropsOf; allowsJSX?: boolean; }): ShorthandFactory

; export function createShorthandFactory

({ Component, mappedProp, mappedArrayProp, allowsJSX }) { if (!ReactIs.isValidElementType(Component)) { throw new Error('createShorthandFactory() Component must be a string or function.'); } return (value, options: CreateShorthandOptions

) => createShorthandInternal({ allowsJSX, Component, mappedProp, mappedArrayProp, value, options, }); } // ============================================================ // Factories // ============================================================ export function createShorthandInternal

({ Component, mappedProp, mappedArrayProp, value, options = {}, allowsJSX = true, }: { Component: React.ElementType

; mappedProp?: string; mappedArrayProp?: string; allowsJSX?: boolean; value?: ShorthandValue

; options?: CreateShorthandOptions

; }) { if (!ReactIs.isValidElementType(Component)) { throw new Error('createShorthand() Component must be a string or function.'); } // short circuit noop values const valIsNoop = _.isNil(value) || typeof value === 'boolean'; if (valIsNoop && !options.render) return null; const valIsPrimitive = typeof value === 'string' || typeof value === 'number'; const valIsPropsObject = _.isPlainObject(value); const valIsArray = _.isArray(value); const valIsReactElement = React.isValidElement(value); // unhandled type warning if (process.env.NODE_ENV !== 'production') { const displayName = typeof Component === 'string' ? Component : Component.displayName; if (!valIsPrimitive && !valIsPropsObject && !valIsArray && !valIsReactElement && !valIsNoop) { /* eslint-disable-next-line no-console */ console.error( [ `The shorthand prop for "${displayName}" component was passed a JSX element but this slot only supports string|number|object|array|ReactElements.`, ' Use null|undefined|boolean for none.', ` Received: ${value}`, ].join(''), ); } if (!allowsJSX && valIsReactElement) { /* eslint-disable-next-line no-console */ console.error( [ `The shorthand prop for "${displayName}" component was passed a JSX element but this slot only supports string|number|object|array.`, ' Use null|undefined|boolean for none.', ` Received: ${value}`, ].join(''), ); } } // ---------------------------------------- // Build up props // ---------------------------------------- const defaultProps = options.defaultProps ? options.defaultProps() || ({} as Props

) : ({} as Props

); // User's props const usersProps = (valIsReactElement && ({} as Props

)) || (valIsPropsObject && (value as Props

)) || ({} as Props

); // Override props const overrideProps: Props

= typeof options.overrideProps === 'function' ? (options.overrideProps({ ...defaultProps, ...usersProps }) as Props

) : (options.overrideProps as Props

) || ({} as Props

); // Merge props const props: Props

= { ...defaultProps, ...usersProps, ...overrideProps }; const mappedHTMLProps = mappedProps[overrideProps.as || defaultProps.as]; // Map prop for primitive value if (valIsPrimitive || valIsReactElement) { (props as any)[mappedHTMLProps || mappedProp || 'children'] = value; } // Map prop for array value if (valIsArray) { (props as any)[mappedHTMLProps || mappedArrayProp || 'children'] = value; } // Merge className if (defaultProps.className || overrideProps.className || usersProps.className) { const mergedClassesNames = cx(defaultProps.className, overrideProps.className, usersProps.className); (props as any).className = _.uniq(mergedClassesNames.split(' ')).join(' '); } // Merge style if (defaultProps.style || overrideProps.style || usersProps.style) { (props as any).style = { ...defaultProps.style, ...usersProps.style, ...overrideProps.style }; } // Merge styles if (defaultProps.styles || overrideProps.styles || usersProps.styles) { (props as any).styles = mergeStyles(defaultProps.styles, usersProps.styles, overrideProps.styles); } // ---------------------------------------- // Get key // ---------------------------------------- const { generateKey = true } = options; // Use key or generate key if (generateKey && _.isNil(props.key)) { if (valIsPrimitive) { // use string/number shorthand values as the key (props as any).key = value; } if (valIsReactElement) { // use the key from React Element const elementKey = (value as React.ReactElement).key; //

- key is not passed as will be `null` //
- key is passed as `null` and will be stringified const isNullKey = elementKey === null; if (!isNullKey) { (props as any).key = elementKey; } } } // Remove the kind prop from the props object delete props.kind; // ---------------------------------------- // Create Element // ---------------------------------------- const { render } = options; if (render) { return render(Component, props); } if (typeof props.children === 'function') { return props.children(Component, { ...props, children: undefined }); } if (!allowsJSX && valIsReactElement) { return null; } // Create ReactElements from built up props if (valIsPrimitive || valIsPropsObject || valIsArray || valIsReactElement) { return React.createElement(Component, props); } return null; } export function createShorthand( Component: TFunctionComponent & { shorthandConfig?: ShorthandConfig>; fluentComposeConfig?: ComposePreparedOptions>; }, value?: ShorthandValue>, options?: CreateShorthandOptions>, ): React.ReactElement; export function createShorthand( Component: { new (...args: any[]): TInstance } & { shorthandConfig?: ShorthandConfig>; fluentComposeConfig?: ComposePreparedOptions>; }, value?: ShorthandValue>, options?: CreateShorthandOptions>, ): React.ReactElement; export function createShorthand( Component: ComponentWithAs & { shorthandConfig?: ShorthandConfig

; fluentComposeConfig?: ComposePreparedOptions

; }, value?: ShorthandValue

, options?: CreateShorthandOptions

, ): React.ReactElement; export function createShorthand( Component: TElementType & { shorthandConfig?: ShorthandConfig>; fluentComposeConfig?: ComposePreparedOptions>; }, value?: ShorthandValue>, options?: CreateShorthandOptions>, ): React.ReactElement; export function createShorthand

(Component, value?, options?) { const { mappedProp = 'children', allowsJSX = true, mappedArrayProp } = Component.shorthandConfig || Component.fluentComposeConfig?.shorthandConfig || {}; return createShorthandInternal

({ Component, mappedProp, allowsJSX, mappedArrayProp, value, options: options || {}, }); }