@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
487 lines • 23.6 kB
TypeScript
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