@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
382 lines (381 loc) • 16.9 kB
JavaScript
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;
;