@diskette/use-render 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # useRender
2
+
3
+ _description_
4
+
5
+ ## License
6
+
7
+ [MIT](./LICENSE) License ©
@@ -0,0 +1,2 @@
1
+ export { useRender } from './use-render.ts';
2
+ export type { ComponentProps, UseRenderOptions } from './use-render.ts';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { useRender } from "./use-render.js";
@@ -0,0 +1,6 @@
1
+ import type { ComponentProps, CSSProperties, ElementType, ReactNode } from 'react';
2
+ export type ClassNameResolver<State> = ((state: State, defaultClassName?: string | undefined) => string | undefined) | string | undefined;
3
+ export type StyleResolver<State> = ((state: State, defaultStyle?: CSSProperties | undefined) => CSSProperties | undefined) | CSSProperties | undefined;
4
+ export type Renderer<T extends ElementType, S> = (state: S, props: ComponentProps<T>) => ReactNode;
5
+ export type DataAttributes = Record<`data-${string}`, string | number | boolean>;
6
+ export type BaseComponentProps<T extends ElementType> = Omit<ComponentProps<T>, 'children' | 'className' | 'style'>;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ type Ref<Instance> = Exclude<React.Ref<Instance>, React.RefObject<Instance>> | React.RefObject<Instance | null>;
3
+ /**
4
+ * Merges an array of refs into a single memoized callback ref or `null`.
5
+ * @see https://floating-ui.com/docs/react-utils#usemergerefs
6
+ */
7
+ export declare function useMergeRefs<Instance>(refs: Array<Ref<Instance> | undefined>): null | React.RefCallback<Instance>;
8
+ export {};
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ /**
3
+ * Merges an array of refs into a single memoized callback ref or `null`.
4
+ * @see https://floating-ui.com/docs/react-utils#usemergerefs
5
+ */
6
+ export function useMergeRefs(refs) {
7
+ const cleanupRef = React.useRef(undefined);
8
+ const refEffect = React.useCallback((instance) => {
9
+ const cleanups = refs.map((ref) => {
10
+ if (ref == null) {
11
+ return;
12
+ }
13
+ if (typeof ref === 'function') {
14
+ const refCallback = ref;
15
+ const refCleanup = refCallback(instance);
16
+ return typeof refCleanup === 'function'
17
+ ? refCleanup
18
+ : () => {
19
+ refCallback(null);
20
+ };
21
+ }
22
+ ref.current = instance;
23
+ return () => {
24
+ ref.current = null;
25
+ };
26
+ });
27
+ return () => {
28
+ cleanups.forEach((refCleanup) => refCleanup?.());
29
+ };
30
+ }, refs);
31
+ return React.useMemo(() => {
32
+ if (refs.every((ref) => ref == null)) {
33
+ return null;
34
+ }
35
+ return (value) => {
36
+ if (cleanupRef.current) {
37
+ cleanupRef.current();
38
+ cleanupRef.current = undefined;
39
+ }
40
+ if (value != null) {
41
+ cleanupRef.current = refEffect(value);
42
+ }
43
+ };
44
+ }, refs);
45
+ }
@@ -0,0 +1,19 @@
1
+ import type { ElementType, JSX, ReactNode } from 'react';
2
+ import type { BaseComponentProps, ClassNameResolver, DataAttributes, Renderer, StyleResolver } from './types.ts';
3
+ export type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
4
+ children?: ((state: S) => ReactNode) | ReactNode;
5
+ className?: ClassNameResolver<S>;
6
+ style?: StyleResolver<S>;
7
+ render?: Renderer<T, S> | JSX.Element;
8
+ };
9
+ export interface UseRenderOptions<T extends ElementType, S> {
10
+ defaultProps?: React.ComponentProps<T> & DataAttributes;
11
+ props?: ComponentProps<T, S> & DataAttributes;
12
+ ref?: React.Ref<any> | (React.Ref<any> | undefined)[];
13
+ state: S;
14
+ }
15
+ /**
16
+ * Hook for enabling a render prop in custom components. Designed to be used by component libraries as an implementation detail
17
+ * in providing a way to override a component's default rendered element.
18
+ */
19
+ export declare function useRender<T extends ElementType, S>(tag: T, options: UseRenderOptions<T, S>): JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { cloneElement, createElement, isValidElement, useMemo } from 'react';
2
+ import { useMergeRefs } from "./use-merge-refs.js";
3
+ import { isFunction, isUndefined, resolveClassName, resolveStyle, } from "./utils.js";
4
+ /**
5
+ * Hook for enabling a render prop in custom components. Designed to be used by component libraries as an implementation detail
6
+ * in providing a way to override a component's default rendered element.
7
+ */
8
+ export function useRender(tag, options) {
9
+ const { defaultProps, props = {}, ref: optionsRef, state } = options;
10
+ const defaultClassName = defaultProps?.className;
11
+ const defaultStyle = defaultProps?.style;
12
+ const defaultChildren = defaultProps?.children;
13
+ const { children, className: propsClassName, style: propsStyle, ref: propsRef, render, ...restProps } = defaultProps
14
+ ? { ...defaultProps, ...props }
15
+ : props;
16
+ const resolvedClassName = resolveClassName(defaultClassName, propsClassName, state);
17
+ const resolvedStyle = resolveStyle(defaultStyle, propsStyle, state);
18
+ const refs = useMemo(() => {
19
+ const refsArr = Array.isArray(optionsRef) ? optionsRef : [optionsRef];
20
+ if (propsRef) {
21
+ refsArr.push(propsRef);
22
+ }
23
+ return refsArr;
24
+ }, [optionsRef, propsRef]);
25
+ const mergedRef = useMergeRefs(refs);
26
+ const resolvedProps = {
27
+ ...restProps,
28
+ ...(!isUndefined(resolvedClassName) && { className: resolvedClassName }),
29
+ ...(!isUndefined(resolvedStyle) && { style: resolvedStyle }),
30
+ ...(mergedRef !== null && { ref: mergedRef }),
31
+ };
32
+ const resolvedChildren = isFunction(children) ? children(state) : children;
33
+ if (isValidElement(render)) {
34
+ return cloneElement(render, resolvedProps, resolvedChildren ?? defaultChildren);
35
+ }
36
+ if (isFunction(render)) {
37
+ return render(state, {
38
+ ...resolvedProps,
39
+ children: resolvedChildren,
40
+ });
41
+ }
42
+ return createElement(tag, resolvedProps, resolvedChildren ?? defaultChildren);
43
+ }
@@ -0,0 +1,18 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { ClassNameResolver, StyleResolver } from './types.ts';
3
+ export declare const isFunction: (value: unknown) => value is Function;
4
+ export declare const isUndefined: (value: unknown) => value is undefined;
5
+ /**
6
+ * Resolves and merges className values from defaultProps and props.
7
+ * - Resolves function values with the provided state
8
+ * - Merges string values using clsx
9
+ * - Function props receive the resolved default value
10
+ */
11
+ export declare function resolveClassName<State>(defaultClassName: ClassNameResolver<State>, propsClassName: ClassNameResolver<State>, state: State): string | undefined;
12
+ /**
13
+ * Resolves and merges style values from defaultProps and props.
14
+ * - Resolves function values with the provided state
15
+ * - Merges object values using object spread
16
+ * - Function props receive the resolved default value
17
+ */
18
+ export declare function resolveStyle<State>(defaultStyle: StyleResolver<State>, propsStyle: StyleResolver<State>, state: State): CSSProperties | undefined;
package/dist/utils.js ADDED
@@ -0,0 +1,49 @@
1
+ import { clsx } from 'clsx';
2
+ export const isFunction = (value) => typeof value === 'function';
3
+ export const isUndefined = (value) => typeof value === 'undefined';
4
+ /**
5
+ * Resolves and merges className values from defaultProps and props.
6
+ * - Resolves function values with the provided state
7
+ * - Merges string values using clsx
8
+ * - Function props receive the resolved default value
9
+ */
10
+ export function resolveClassName(defaultClassName, propsClassName, state) {
11
+ const resolvedDefault = isFunction(defaultClassName)
12
+ ? defaultClassName(state)
13
+ : defaultClassName;
14
+ if (!isUndefined(propsClassName)) {
15
+ if (isFunction(propsClassName)) {
16
+ return propsClassName(state, resolvedDefault);
17
+ }
18
+ else {
19
+ return clsx(resolvedDefault, propsClassName);
20
+ }
21
+ }
22
+ else {
23
+ return resolvedDefault;
24
+ }
25
+ }
26
+ /**
27
+ * Resolves and merges style values from defaultProps and props.
28
+ * - Resolves function values with the provided state
29
+ * - Merges object values using object spread
30
+ * - Function props receive the resolved default value
31
+ */
32
+ export function resolveStyle(defaultStyle, propsStyle, state) {
33
+ const resolvedDefault = isFunction(defaultStyle)
34
+ ? defaultStyle(state)
35
+ : defaultStyle;
36
+ if (!isUndefined(propsStyle)) {
37
+ if (isFunction(propsStyle)) {
38
+ return propsStyle(state, resolvedDefault);
39
+ }
40
+ else {
41
+ return resolvedDefault
42
+ ? { ...resolvedDefault, ...propsStyle }
43
+ : propsStyle;
44
+ }
45
+ }
46
+ else {
47
+ return resolvedDefault;
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@diskette/use-render",
3
+ "type": "module",
4
+ "version": "0.2.0",
5
+ "exports": "./dist/index.js",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "peerDependencies": {
10
+ "@types/react": "*",
11
+ "react": "^18.0 || ^19.0"
12
+ },
13
+ "peerDependenciesMeta": {
14
+ "@types/react": {
15
+ "optional": true
16
+ }
17
+ },
18
+ "devDependencies": {
19
+ "@changesets/cli": "^2.29.7",
20
+ "@types/react": "^19.2.6",
21
+ "@vitejs/plugin-react": "^5.1.1",
22
+ "@vitest/browser-playwright": "^4.0.13",
23
+ "oxlint": "^1.29.0",
24
+ "oxlint-tsgolint": "^0.8.1",
25
+ "prettier": "^3.6.2",
26
+ "react": "^19.2.0",
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^4.0.13",
29
+ "vitest-browser-react": "^2.0.2"
30
+ },
31
+ "description": "_description_",
32
+ "homepage": "https://github.com/diskettejs/use-render#readme",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/diskettejs/use-render.git"
36
+ },
37
+ "keywords": [],
38
+ "prettier": {
39
+ "semi": false,
40
+ "singleQuote": true
41
+ },
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "clsx": "^2.1.1"
45
+ },
46
+ "scripts": {
47
+ "dev": "tsc --watch -p tsconfig.build.json",
48
+ "build": "rm -rf && tsc -p tsconfig.build.json",
49
+ "test": "vitest run",
50
+ "typecheck": "tsc",
51
+ "lint": "oxlint --type-aware src",
52
+ "release": "changeset publish && git push --follow-tags"
53
+ }
54
+ }