@openmrs/esm-styleguide
Version:
The styleguide for OpenMRS SPA
99 lines (86 loc) • 3.63 kB
text/typescript
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];
}