UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

382 lines (381 loc) 16.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.composeHooks = exports.useModelContext = exports.useDefaultModel = exports.useLocalRef = exports.useForkRef = exports.createSubModelElemPropsHook = exports.subModelHook = exports.createHook = exports.createElemPropsHook = exports.createComponent = exports.createSubcomponent = exports.createContainer = void 0; const react_1 = __importDefault(require("react")); const assert_1 = require("./assert"); const memoize_1 = require("./memoize"); const mergeProps_1 = require("./mergeProps"); function defaultGetElemProps(input) { return input; } const createContainer = (as) => ({ displayName, modelHook, elemPropsHook, defaultContext, subComponents, }) => { (0, assert_1.assert)(modelHook.Context, 'createContainer only works on models with context. Please use `createModelHook` to create the `modelHook`'); const Context = modelHook.Context; return (Component) => { const ReturnedComponent = react_1.default.forwardRef(({ as: asOverride, model, ...props }, ref) => { const localModel = useDefaultModel(model, props, modelHook, asOverride); const elemProps = (modelHook.getElemProps || defaultGetElemProps)(props); const finalElemProps = elemPropsHook ? elemPropsHook(localModel, elemProps, ref) : elemProps; // make sure there's always a ref being passed, even if there are no elemProps hooks to run if (ref && !finalElemProps.hasOwnProperty('ref')) { finalElemProps.ref = ref; } return react_1.default.createElement(Context.Provider, { value: localModel }, Component(finalElemProps, // Cast to `any` to avoid: "ts(2345): Type 'undefined' is not assignable to type 'E extends // undefined ? never : E'" I'm not sure I can actually cast to this conditional type and it // doesn't actually matter, so cast to `any` it is. (asOverride || as), localModel)); }); Object.keys(subComponents || {}).forEach(key => { // `ReturnedComponent` is a `React.ForwardRefExoticComponent` which has no additional keys so // we'll cast to `Record<string, any>` for assignment. Note the lack of type checking // properties. Take care when changing the runtime of this function. ReturnedComponent[key] = subComponents[key]; // Add a displayName if one isn't already created. This prevents us from having to add // `displayName` to subcomponents if a container component has a `displayName` if (displayName && !subComponents[key].displayName) { subComponents[key].displayName = `${displayName}.${key}`; } }); ReturnedComponent.displayName = displayName; ReturnedComponent.__hasModel = true; // The `any`s are here because `ElementComponent` takes care of the `as` type and the // `ReturnComponent` type is overridden ReturnedComponent.as = (0, memoize_1.memoize)((as) => (0, exports.createContainer)(as)({ displayName, subComponents, modelHook, elemPropsHook })(Component), as => as); // Cast as `any`. We have already specified the return type. Be careful making changes to this // file due to this `any` `ReturnedComponent` is a `React.ForwardRefExoticComponent`, but we want // it to be either an `Component` or `ElementComponent` return ReturnedComponent; }; }; exports.createContainer = createContainer; const createSubcomponent = (as) => ({ displayName, modelHook, elemPropsHook, subComponents, }) => { (0, assert_1.assert)(modelHook.Context, 'createSubcomponent only works on models with context. Please use `createModelHook` to create the `modelHook`'); return (Component) => { const ReturnedComponent = react_1.default.forwardRef(({ as: asOverride, model, elemPropsHook: additionalPropsHook, ...props }, ref) => { const localModel = useModelContext(modelHook.Context, model, asOverride); // maybeModelProps reattached the `model` prop if the passed model is incompatible with the // modelHook's context. This fixes issues when using the `as` prop on model element components // that both have a model const maybeModelProps = model && localModel !== model ? { ...props, model, ref } : { ...props, ref }; const elemProps = elemPropsHook ? elemPropsHook(localModel, maybeModelProps, ref) : maybeModelProps; return Component(additionalPropsHook ? additionalPropsHook(localModel, elemProps, ref) : elemProps, // Cast to `any` to avoid: "ts(2345): Type 'undefined' is not assignable to type 'E extends // undefined ? never : E'" I'm not sure I can actually cast to this conditional type and it // doesn't actually matter, so cast to `any` it is. (asOverride || as), localModel); }); Object.keys(subComponents || {}).forEach(key => { // `ReturnedComponent` is a `React.ForwardRefExoticComponent` which has no additional keys so // we'll cast to `Record<string, any>` for assignment. Note the lack of type checking // properties. Take care when changing the runtime of this function. ReturnedComponent[key] = subComponents[key]; }); if (displayName) { ReturnedComponent.displayName = displayName; } ReturnedComponent.__hasModel = true; // The `any`s are here because `ElementComponent` takes care of the `as` type and the // `ReturnComponent` type is overridden ReturnedComponent.as = (0, memoize_1.memoize)((as) => (0, exports.createSubcomponent)(as)({ displayName, subComponents, modelHook, elemPropsHook })(Component), as => as); // Cast as `any`. We have already specified the return type. Be careful making changes to this // file due to this `any` `ReturnedComponent` is a `React.ForwardRefExoticComponent`, but we want // it to be either an `Component` or `ElementComponent` return ReturnedComponent; }; }; exports.createSubcomponent = createSubcomponent; /** * Factory function that creates components to be exported. It enforces React ref forwarding, `as` * prop, display name, and sub-components, and handles proper typing without much boiler plate. The * return type is `Component<element, Props>` which looks like `Component<'div', Props>` which is a * clean interface that tells you the default element that is used. */ const createComponent = (as) => ({ displayName, Component, subComponents, }) => { const ReturnedComponent = react_1.default.forwardRef(({ as: asOverride, ...props }, ref) => { return Component(props, ref, // Cast to `any` to avoid: "ts(2345): Type 'undefined' is not assignable to type 'E extends // undefined ? never : E'" I'm not sure I can actually cast to this conditional type and it // doesn't actually matter, so cast to `any` it is. (asOverride || as)); }); Object.keys(subComponents || {}).forEach(key => { // `ReturnedComponent` is a `React.ForwardRefExoticComponent` which has no additional keys so // we'll cast to `Record<string, any>` for assignment. Note the lack of type checking // properties. Take care when changing the runtime of this function. ReturnedComponent[key] = subComponents[key]; }); ReturnedComponent.displayName = displayName; // The `any`s are here because `ElementComponent` takes care of the `as` type and the // `ReturnComponent` type is overridden ReturnedComponent.as = (0, memoize_1.memoize)((as) => (0, exports.createComponent)(as)({ displayName, Component, subComponents }), as => as); // Cast as `any`. We have already specified the return type. Be careful making changes to this // file due to this `any` `ReturnedComponent` is a `React.ForwardRefExoticComponent`, but we want // it to be either an `Component` or `ElementComponent` return ReturnedComponent; }; exports.createComponent = createComponent; /** * An `elemPropsHook` is a React hook that takes a model, ref, and elemProps and returns props and * attributes to be spread to an element or component. * * ```tsx * const useMyHook = createElemPropsHook(useMyModel)((model) => { * return { * id: model.state.id * } * }) * ``` * * **Note:** If your hook needs to use a ref, it must be forked using `useLocalRef` or `useForkRef` * and return the forked ref: * * ```tsx * const useMyHook = createElemPropsHook(useMyModel)((model, ref, elemProps) => { * const {localRef, elementRef} = useLocalRef(ref); * * React.useLayoutEffect(() => { * console.log('element', localRef.current) // logs the DOM element * }, []) * * return { * ref: elementRef * } * }) * ``` */ const createElemPropsHook = (modelHook) => (fn) => { return ((model, elemProps, ref) => { const props = (0, mergeProps_1.mergeProps)(fn(model, ref, elemProps || {}), elemProps || {}); if (!props.hasOwnProperty('ref') && ref) { // This is the weird "incoming ref isn't in props, but outgoing ref is in props" thing // @ts-ignore TS says `ref` isn't on `PO`, but we always add it anyways props.ref = ref; } return props; }); }; exports.createElemPropsHook = createElemPropsHook; /** * Factory function to crate a behavior hook with correct generic types. It takes a function that * return props and returns a function that will also require `elemProps` and will call `mergeProps` for * you. If your hook makes use of the `ref`, you will have to also use `useLocalRef` to properly fork * the ref. * * @example * const useMyHook = createHook((model: MyModel, ref) => { * const { localRef, elementRef } = useLocalRef(ref); * // do whatever with `localRef` which is a RefObject * * return { * onClick: model.events.doSomething, * ref: elementRef, * }; * }); * * // Equivalent to: * const useMyHook = <P extends {}>( * model: MyModel, * elemProps: P, * ref: React.Ref<unknown> * ) => { * const { localRef, elementRef } = useLocalRef(ref); * // do whatever with `localRef` which is a RefObject * * return mergeProps({ * onClick: model.events.doSomething, * ref: elementRef, * }, elemProps); * }; * * @param fn Function that takes a model and optional ref and returns props */ const createHook = (fn) => { return ((model, elemProps, ref) => { const props = (0, mergeProps_1.mergeProps)(fn(model, ref, elemProps || {}), elemProps || {}); if (!props.hasOwnProperty('ref') && ref) { // This is the weird "incoming ref isn't in props, but outgoing ref is in props" thing // @ts-ignore TS says `ref` isn't on `PO`, but we always add it anyways props.ref = ref; } return props; }); }; exports.createHook = createHook; /** * @deprecated ⚠️ `subModelHook` has been deprecated and will be removed in a future major version. Please use `createSubModelElemPropsHook` instead. */ const subModelHook = (fn, hook) => { return ((model, props, ref) => { return hook(fn(model), props, ref); }); }; exports.subModelHook = subModelHook; /** * Creates an elemPropsHook that returns the elemProps from another hook that is meant for a * subModel. Usually only used when composing elemProps hooks. * * For example: * * ```tsx * const useMySubModel = () => {} * * const useMyModel = () => { * const subModel = useMySubModel() * * return { * state, * events, * subModel, * } * } * * const useMyComponent = composeHook( * createElemPropsHook(useMyModel)(model => ({ id: '' })), * createSubModelElemPropsHook(useMyModel)(m => m.subModel, useSomeOtherComponent) * ) * ``` */ function createSubModelElemPropsHook(modelHook) { return (fn, elemPropsHook) => { return ((model, props, ref) => { return elemPropsHook(fn(model), props, ref); }); }; } exports.createSubModelElemPropsHook = createSubModelElemPropsHook; function setRef(ref, value) { if (ref) { if (typeof ref === 'function') { ref(value); } else { // Refs are readonly, but we can technically write to it without issue ref.current = value; } } } /** * This function will create a new forked ref out of two input Refs. This is useful for components * that use `React.forwardRef`, but also need internal access to a Ref. * * This function is inspired by https://www.npmjs.com/package/@rooks/use-fork-ref * * @example * React.forwardRef((props, ref) => { * // Returns a RefObject with a `current` property * const myRef = React.useRef(ref) * * // Returns a forked Ref function to pass to an element. * // This forked ref will update both `myRef` and `ref` when React updates the element ref * const elementRef = useForkRef(ref, myRef) * * useEffect(() => { * console.log(myRef.current) // `current` is the DOM instance * // `ref` might be null since it depends on if someone passed a `ref` to your component * // `elementRef` is a function and we cannot get a current value out of it * }) * * return <div ref={elementRef}/> * }) */ function useForkRef(ref1, ref2) { return (value) => { setRef(ref1, value); setRef(ref2, value); }; } exports.useForkRef = useForkRef; /** * This functions handles the common use case where a component needs a local ref and needs to * forward a ref to an element. * @param ref The React ref passed from the `createComponent` factory function * * @example * const MyComponent = ({children, ...elemProps}: MyProps, ref, Element) => { * const { localRef, elementRef } = useLocalRef(ref); * * // do something with `localRef` which is a `RefObject` with a `current` property * * return <Element ref={elementRef} {...elemProps} /> * } */ function useLocalRef(ref) { const localRef = react_1.default.useRef(null); const elementRef = useForkRef(ref, localRef); return { localRef, elementRef }; } exports.useLocalRef = useLocalRef; /** * Returns a model, or calls the model hook with config. Clever way around the conditional React * hook ESLint rule. * @param model A model, if provided * @param config Config for a model * @param modelHook A model hook that takes valid config * @example * const ContainerComponent = ({children, model, ...config}: ContainerProps) => { * const value = useDefaultModel(model, config, useContainerModel); * * // ... * } */ function useDefaultModel(model, config, modelHook, as) { // Make sure we don't pass the `model` to a component if it is incompatible with that component. // Otherwise we'll have strange runtime failures when a component or elemProps hooks try to // access the `state` or `events` if (!model || (as && as.__hasModel && model.__UNSTABLE_modelContext !== modelHook.Context)) { return modelHook(config); } return model; } exports.useDefaultModel = useDefaultModel; /** * Returns a model, or returns a model context. Clever way around the conditional React hook ESLint * rule * @param model A model, if provided * @param context The context of a model * @example * const SubComponent = ({children, model, ...elemProps}: SubComponentProps, ref, Element) => { * const {state, events} = useModelContext(model, SubComponentModelContext, Element); * * // ... * } */ function useModelContext(context, model, as) { const contextModel = react_1.default.useContext(context); if (!model || (as && as.__hasModel && model.__UNSTABLE_modelContext !== context)) { return contextModel; } return model; } exports.useModelContext = useModelContext; function composeHooks(...hooks) { return ((model, props, ref) => { const returnProps = [...hooks].reverse().reduce((props, hook) => { return hook(model, props, props.ref || ref); }, props); // remove null props values for (const key in returnProps) { if (returnProps[key] === null) { delete returnProps[key]; } } if (!returnProps.hasOwnProperty('ref') && ref) { // This is the weird "incoming ref isn't in props, but outgoing ref is in props" thing returnProps.ref = ref; } return returnProps; }); } exports.composeHooks = composeHooks;