UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

487 lines 23.6 kB
import React from 'react'; import { MergeProps, RemoveNulls } from './mergeProps'; import { Model } from './models'; /** * Adds the `as` to the style interface to support `as` in styled components */ export type StyledType = { as?: React.ElementType; }; type Constructor<T> = new (...args: any[]) => T; /** * Extract a Ref from T if it exists * This will return the following: * * - `undefined` => `never` * - `'button'` => `React.Ref<HTMLButtonElement>` * - `ElementComponent<'button', ButtonProps>` => `React.Ref<HTMLButtonElement>` */ type ExtractRef<T> = T extends undefined ? never : T extends Constructor<infer C> ? React.LegacyRef<C> : React.Ref<T extends keyof ElementTagNameMap ? ElementTagNameMap[T] : T extends ElementComponent<infer U, any> ? U extends keyof ElementTagNameMap ? ElementTagNameMap[U] : U : T extends React.FC<{ ref?: infer R; }> ? R extends React.RefObject<infer E> ? E : never : T>; /** * Generic component props with "as" prop * @template P Additional props * @template ElementType React component or string element */ export type PropsWithoutAs<P, ElementType extends React.ElementType> = P & Omit<React.ComponentProps<ElementType>, 'as' | keyof P> & { /** * Optional ref. If the component represents an element, this ref will be a reference to the * real DOM element of the component. If `as` is set to an element, it will be that element. * If `as` is a component, the reference will be to that component (or element if the component * uses `React.forwardRef`). */ ref?: ExtractRef<ElementType>; }; /** * Extracts only the HTML attribute interface from `JSX.IntrinsicElements[E]`. This effectively removes `ref` and `key` * without using `Omit` which makes the returned type more difficult to understand. * * For example: * JSX.IntrinsicElements['button'] // React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement> * ExtractHTMLAttributes<JSX.IntrinsicElements['button']> // React.HTMLButtonAttributes<HTMLButtonElement> */ type ExtractHTMLAttributes<T extends React.DetailedHTMLProps<any, any>> = T extends React.DetailedHTMLProps<infer P, any> ? P : T; /** * Extract props from any component that was created using `createComponent`. It will return the * HTML attribute interface of the default element used with `createComponent`. If you use `as`, the * HTML attribute interface can change, so you can use an override to the element you wish to use. * You can also disable the HTML attribute by passing `never`: * * - `ExtractProps<typeof Card>`: `CardProps & React.HTMLAttributes<HTMLDivElement>` * - `ExtractProps<typeof Card, 'aside'>`: `CardProps & React.HTMLAttributes<HTMLElement>` * - `ExtractProps<typeof Card, never>`: `CardProps` * * @template TComponent The component you wish to extract props from. Needs 'typeof` in front: * `typeof Card` * @template TElement An optional override of the element that will be used. Define this if you use * an `as` on the component * * @example * interface MyProps extends ExtractProps<typeof Card.Body> {} * * ExtractProps<typeof Card>; // CardProps & React.HTMLAttributes<HTMLDivElement> * ExtractProps<typeof Card, 'aside'>; // CardProps & React.HTMLAttributes<HTMLElement> * ExtractProps<typeof Card, never>; // CardProps */ export type ExtractProps<TComponent, TElement extends keyof JSX.IntrinsicElements | ElementComponent<any, any> | Component<any> | React.ComponentType<any> | undefined | never = undefined> = ExtractMaybeModel<TComponent, TComponent extends { __element: infer E; __props: infer P; } ? [TElement] extends [never] ? P : TElement extends undefined ? E extends keyof JSX.IntrinsicElements ? P & ExtractHTMLAttributes<JSX.IntrinsicElements[E]> : P & ExtractProps<E> : TElement extends keyof JSX.IntrinsicElements ? P & ExtractHTMLAttributes<JSX.IntrinsicElements[TElement]> : P & ExtractProps<TElement> : TComponent extends { __props: infer P; } ? P : TComponent extends React.ComponentType<infer P> ? P : {}>; type ExtractMaybeModel<TComponent, P> = TComponent extends { __model: infer M; } ? P & PropsWithModel<M> : P; /** * `PropsWithModel` adds the `model` and `elemPropsHook` props. If a model-based component has an * `as` referencing another model-based component, the `model` of the `as` component should win. */ export type PropsWithModel<TModel, ElementType = {}> = { /** * Optional model to pass to the component. This will override the default model created for the * component. This can be useful if you want to access to the state and events of the model, or if * you have nested components of the same type and you need to override the model provided by * React Context. */ model?: ElementType extends { __model: infer M; } ? M : TModel; /** * Optional hook that receives the model and all props to be applied to the element. If you use * this, it is your responsibility to return props, merging as appropriate. For example, returning * an empty object will disable all elemProps hooks associated with this component. This allows * finer control over a component without creating a new one. */ elemPropsHook?: <TProps>(model: TModel, elemProps: TProps) => any; }; /** * Component type that allows for `as` to change the element or component type. * Passing `as` will correctly change the allowed interface of the JSX element */ export type ElementComponent<E extends React.ElementType, P> = { displayName?: string; <ElementType extends React.ElementType>(props: PropsWithoutAs<P, ElementType> & { /** * Optional override of the default element used by the component. Any valid tag or Component. * If you provided a Component, this component should forward the ref using `React.forwardRef` * and spread extra props to a root element. */ as: ElementType; }): JSX.Element; (props: PropsWithoutAs<P, E>): JSX.Element; as<E extends React.ElementType>(as: E): ElementComponent<E, P>; /** @private Only used internally to hold the element type for extraction */ __element: E; /** @private Only used internally to hold the element type for extraction */ __props: P; }; /** * Component type that allows for `as` to change the element or component type. * Passing `as` will correctly change the allowed interface of the JSX element. * Same as `ElementComponent`, but adds a model to the interface. */ export type ElementComponentM<E extends React.ElementType, P, TModel> = { displayName?: string; <ElementType extends React.ElementType>(props: PropsWithoutAs<P, ElementType> & PropsWithModel<TModel, ElementType> & { /** * Optional override of the default element used by the component. Any valid tag or Component. * If you provided a Component, this component should forward the ref using `React.forwardRef` * and spread extra props to a root element. */ as: ElementType; }): JSX.Element; (props: PropsWithoutAs<P, E> & PropsWithModel<TModel>): JSX.Element; as<E extends React.ElementType>(as: E): ElementComponentM<E, P, TModel>; /** @private Only used internally to hold the element type for extraction */ __element: E; /** @private Only used internally to hold the element type for extraction */ __props: P; /** @private Only used internally to hold the element type for extraction */ __model: TModel; }; export type ComponentM<P, TModel> = { displayName?: string; (props: P & PropsWithModel<TModel>): JSX.Element; /** @private Only used internally to hold the element type for extraction */ __props: P; /** @private Only used internally to hold the element type for extraction */ __model: TModel; }; export type Component<P> = { displayName?: string; (props: P): JSX.Element; /** @private Only used internally to hold the element type for extraction */ __props: P; }; interface RefForwardingComponent<T, P = {}> { (props: React.PropsWithChildren<P>, /** * A ref to be forwarded. Pass it along to the root element. If no element was passed, this * will result in a `never` */ ref: ExtractRef<T>, /** * An element - either a JSX element or a `ElementComponent`. This should be passed as an `as` * to a root element or be the root element. If no element was passed, this will result in a * `never` */ Element: T extends undefined ? never : T): JSX.Element | null; } export declare const createContainer: <E extends ElementComponent<any, any> | keyof JSX.IntrinsicElements | React.ComponentType<{}> | ElementComponentM<any, any, any> | undefined = undefined>(as?: E | undefined) => <TModelHook extends ((config: any) => Model<any, any>) & { Context?: React.Context<any> | undefined; } & { defaultConfig?: Record<string, any> | undefined; }, TDefaultContext extends Model<any, any>, TElemPropsHook, SubComponents = {}>({ displayName, modelHook, elemPropsHook, defaultContext, subComponents, }: { displayName?: string | undefined; modelHook: TModelHook; elemPropsHook?: TElemPropsHook | undefined; defaultContext?: TDefaultContext | undefined; subComponents?: SubComponents | undefined; }) => <Props>(Component: (props: CompoundProps<Props, TElemPropsHook, E>, Element: E extends undefined ? never : E, model: TModelHook extends (config: infer TConfig) => infer TModel ? TModel : never) => JSX.Element | null) => (TModelHook extends (config: infer TConfig_1) => infer TModel_1 ? E extends undefined ? ComponentM<Props & TConfig_1, TModel_1> : ElementComponentM<E extends undefined ? React.FC<{}> : E, Props & TConfig_1, TModel_1> : never) & SubComponents; /** * If elemProps returns `null` for a prop, that prop is to be removed from the prop * list. This is useful for passing props to an elemProp hook that should not be exposed * to the DOM element */ type RemoveNull<T> = { [K in keyof T]: Exclude<T[K], null>; }; /** * Props for the compound component based on props, elemPropsHook, and element. It will * return a prop interface according to all these inputs. The following will be added to * the passed in `Props` type: * - The prop interface returned by the `elemPropsHook` function * - if there is no detected `ref` in the `Props` interface, a `ref` will be added based on the element type E * - if there is no detected `children` in the `Props` interface, `children` will be added based on `ReactNode` */ type CompoundProps<Props, TElemPropsHook, E> = Props & (TElemPropsHook extends (...args: any[]) => infer TProps ? RemoveNull<Omit<TProps, 'ref'> & { ref: ExtractRef<E>; }> : { ref: ExtractRef<E>; }) & (Props extends { children: any; } ? {} : { children?: React.ReactNode; }); export declare const createSubcomponent: <E extends ElementComponent<any, any> | keyof JSX.IntrinsicElements | React.ComponentType<{}> | ElementComponentM<any, any, any> | undefined = undefined>(as?: E | undefined) => <TElemPropsHook, TModelHook extends ((config: any) => Model<any, any>) & { Context?: React.Context<any> | undefined; }, SubComponents = {}>({ displayName, modelHook, elemPropsHook, subComponents, }: { /** @deprecated ⚠️ `displayName` has been deprecated and will be removed in a future major version. You no longer need to use `displayName`. A `displayName` will be automatically added if it belongs to a container. */ displayName?: string | undefined; modelHook: TModelHook; elemPropsHook?: TElemPropsHook | undefined; subComponents?: SubComponents | undefined; }) => <Props = {}>(Component: (props: CompoundProps<Props, TElemPropsHook, E>, Element: E extends undefined ? never : E, model: TModelHook extends (...args: any[]) => infer TModel ? TModel : never) => JSX.Element | null) => (TModelHook extends (...args: any[]) => infer TModel_1 ? ElementComponentM<E extends undefined ? React.FC<{}> : E, Props, TModel_1> : never) & SubComponents; /** * 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. */ export declare const createComponent: <E extends ElementComponent<any, any> | keyof JSX.IntrinsicElements | React.ComponentType<{}> | undefined = undefined>(as?: E | undefined) => <P, SubComponents = {}>({ displayName, Component, subComponents, }: { /** This is what the component will look like in the React dev tools. Encouraged to more easily * understand the component tree */ displayName?: string | undefined; /** The component function. The function looks like: * @example * Component: ({children}, ref, Element) { * // `Element` is what's passed to the `as` of your component. If no `as` was defined, it * // will be the default element. It will be 'div' or even a another Component! * return ( * <Element ref={ref}>{children}</Element> * ) * } * * @example * Component: ({children}, ref, Element) { * // `Element` can be passed via `as` to the next component * return ( * <AnotherElement as={Element} ref={ref}>{children}</AnotherElement> * ) * } */ Component: RefForwardingComponent<E, P>; /** * Used in container components */ subComponents?: SubComponents | undefined; }) => (E extends undefined ? Component<P> : ElementComponent<E extends undefined ? React.FC<{}> : E, P>) & SubComponents; /** * 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 * } * }) * ``` */ export declare const createElemPropsHook: <TModelHook extends (config: any) => Model<any, any>>(modelHook: TModelHook) => <const PO extends {}, const PI extends {}>(fn: (model: TModelHook extends (config: any) => infer TModel ? TModel : Model<any, any>, ref?: React.Ref<unknown>, elemProps?: PI | undefined) => PO) => BehaviorHook<TModelHook extends (config: any) => infer TModel_1 ? TModel_1 : Model<any, any>, PO>; /** * 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 */ export declare const createHook: <M extends Model<any, any>, PO extends {}, PI extends {}>(fn: (model: M, ref?: React.Ref<unknown>, elemProps?: PI | undefined) => PO) => BehaviorHook<M, PO>; /** * @deprecated ⚠️ `subModelHook` has been deprecated and will be removed in a future major version. Please use `createSubModelElemPropsHook` instead. */ export declare const subModelHook: <M extends Model<any, any>, SM extends Model<any, any>, O extends {}>(fn: (model: M) => SM, hook: BehaviorHook<SM, O>) => BehaviorHook<M, O>; /** * 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) * ) * ``` */ export declare function createSubModelElemPropsHook<M extends () => Model<any, any>>(modelHook: M): <SM extends Model<any, any>, O extends {}>(fn: (model: ReturnType<M>) => SM, elemPropsHook: BehaviorHook<SM, O>) => BehaviorHook<ReturnType<M>, O>; /** Simplify and speed up inference by capturing types in the signature itself */ interface BaseHook<M extends Model<any, any>, O extends {}> { /** * Capture the model type in TypeScript only. Do not use in runtime! * * @private */ __model: M; /** * Capture the hook's output type in TypeScript only. Do not use in runtime! This is used to cache * and speed up the output types during inference * * @private */ __output: O; } /** * A BehaviorHook is a React hook that takes a model, elemProps, and a ref and returns props and * attributes to apply to an element or component. */ export interface BehaviorHook<M extends Model<any, any>, O extends {}> extends BaseHook<M, O> { <P extends {}>(model: M, elemProps?: P, ref?: React.Ref<unknown>): O & P; } /** * 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}/> * }) */ export declare function useForkRef<T>(ref1?: React.Ref<T>, ref2?: React.Ref<T>): React.RefCallback<T>; /** * 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} /> * } */ export declare function useLocalRef<T>(ref?: React.Ref<T>): { localRef: React.RefObject<T>; elementRef: (instance: T | null) => void; }; /** * 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); * * // ... * } */ export declare function useDefaultModel<T, C>(model: T | undefined, config: C, modelHook: (config: C) => T, as?: React.ElementType): T; /** * 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); * * // ... * } */ export declare function useModelContext<T>(context: React.Context<T>, model?: T, as?: React.ElementType): T; /** * Compose many hooks together. Each hook should make a call to `mergeProps` which is automatically * done by `createElemPropsHook` and `createHook. Returns a function that will receive a model and * return props to be applied to a component. Hooks run from last to first, but props override from * first to last. This means the last hook will run first, passing `elemProps` to the next last * hook. There is a special exception, which is `null`. `null` means "remove this prop" and the null * handling takes precedence to the first. Take care when using `null` as it will remove props * passed in even from the developer. It can be useful when passing data between composed hooks or * then redirecting a prop somewhere else. * * For example: * * ```ts * const useHook1 = createElemPropsHook(useMyModel)((model, ref, elemProps) => { * console.log('useHook1', elemProps) * return { * a: 'useHook1', * c: 'useHook1', * d: null, // remove the `d` prop * } * }) * * const useHook2 = createElemPropsHook(useMyModel)((model, ref, elemProps) => { * console.log('useHook2', elemProps) * return { * b: 'useHook2', * c: 'useHook2', * d: 'useHook2', * } * }) * * const useHook3 = composeHooks( * useHook1, // run last, will have access to `useHook2`'s elemProps, but can remove a prop with `null` * useHook2 // run first and will override all of `useHook1`'s props * ) * const props = useHook3(model, { c: 'props', d: 'props' }) * console.log('props', props) * ``` * * The output would be: * * ```ts * useHook2 {c: 'props', d: 'props'} * useHook1 {b: 'useHook2', c: 'props', d: 'props'} * props {a: 'useHook1', b: 'useHook2', c: 'props', d: null} * ``` */ export declare function composeHooks<H1 extends BaseHook<any, {}>, H2 extends BaseHook<any, {}>, H3 extends BaseHook<any, {}>, H4 extends BaseHook<any, {}>, H5 extends BaseHook<any, {}>, H6 extends BaseHook<any, {}>, H7 extends BaseHook<any, {}>>(hook1: H1, hook2: H2, hook3?: H3, hook4?: H4, hook5?: H5, hook6?: H6, hook7?: H7, ...hooks: BehaviorHook<any, any>[]): H1 extends BaseHook<infer M, infer O1> ? H2 extends BaseHook<any, infer O2> ? H3 extends BaseHook<any, infer O3> ? H4 extends BaseHook<any, infer O4> ? H5 extends BaseHook<any, infer O5> ? H6 extends BaseHook<any, infer O6> ? H7 extends BaseHook<any, infer O7> ? BehaviorHook<M, RemoveNulls<MergeProps<O1, MergeProps<O2, MergeProps<O3, MergeProps<O4, MergeProps<O5, MergeProps<O6, O7>>>>>>>> : never : never : never : never : never : never : never; export {}; //# sourceMappingURL=components.d.ts.map