UNPKG

@openmrs/esm-styleguide

Version:
99 lines (86 loc) 3.63 kB
import { type CSSProperties, type ReactNode, type RefCallback, useCallback, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import { type AriaLabelingProps, type DOMProps } from '@react-types/shared'; // This file contains hooks largely copied from non-exported hooks that are part of // React Aria Components which we need versions of for some of our components. interface RenderPropsHookOptions<T> extends DOMProps, AriaLabelingProps { /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. */ className?: string | ((values: T & { defaultClassName: string | undefined }) => string); /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. A function may be provided to compute the style based on component state. */ style?: CSSProperties | ((values: T & { defaultStyle: CSSProperties }) => CSSProperties | undefined); /** The children of the component. A function may be provided to alter the children based on component state. */ children?: ReactNode | ((values: T & { defaultChildren: ReactNode | undefined }) => ReactNode); values: T; defaultChildren?: ReactNode; defaultClassName?: string; defaultStyle?: CSSProperties; } /** * Hook to provide standard handling for certain properties, especially className, style, and children. */ export function useRenderProps<T>(props: RenderPropsHookOptions<T>) { const { className, style, children, defaultClassName = undefined, defaultChildren = undefined, defaultStyle, values, } = props; return useMemo(() => { let computedClassName: string | undefined; let computedStyle: CSSProperties | undefined; let computedChildren: ReactNode | undefined; if (typeof className === 'function') { computedClassName = className({ ...values, defaultClassName }); } else { computedClassName = className; } if (typeof style === 'function') { computedStyle = style({ ...values, defaultStyle: defaultStyle || {} }); } else { computedStyle = style; } if (typeof children === 'function') { computedChildren = children({ ...values, defaultChildren }); } else if (children == null) { computedChildren = defaultChildren; } else { computedChildren = children; } return { className: computedClassName ?? defaultClassName, style: computedStyle || defaultStyle ? { ...defaultStyle, ...computedStyle } : undefined, children: computedChildren ?? defaultChildren, 'data-rac': '', }; }, [className, style, children, defaultClassName, defaultChildren, defaultStyle, values]); } export function useSlot(initialState: boolean | (() => boolean) = true): [RefCallback<Element>, boolean] { // Initial state is typically based on the parent having an aria-label or aria-labelledby. // If it does, this value should be false so that we don't update the state and cause a rerender when we go through the layoutEffect let [hasSlot, setHasSlot] = useState(initialState); let hasRun = useRef(false); // A callback ref which will run when the slotted element mounts. // This should happen before the useLayoutEffect below. let ref = useCallback((el: Element) => { hasRun.current = true; setHasSlot(!!el); }, []); // If the callback hasn't been called, then reset to false. useLayoutEffect(() => { if (!hasRun.current) { setHasSlot(false); } }, []); return [ref, hasSlot]; }