@diskette/use-render 0.3.0 → 0.5.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 CHANGED
@@ -39,7 +39,7 @@ function Button(props: ButtonProps) {
39
39
  const state: ButtonState = { isPressed, isHovered }
40
40
 
41
41
  return useRender('button', state, {
42
- defaultProps: {
42
+ baseProps: {
43
43
  className: 'btn',
44
44
  onMouseDown: () => setIsPressed(true),
45
45
  onMouseUp: () => setIsPressed(false),
@@ -69,7 +69,7 @@ function Button(props: ButtonProps) {
69
69
 
70
70
  ```tsx
71
71
  <Button
72
- render={(state, props) => (
72
+ render={(props, state) => (
73
73
  <a {...props} href="/path" data-pressed={state.isPressed} />
74
74
  )}
75
75
  >
@@ -83,8 +83,8 @@ Pass a function to resolve className based on component state:
83
83
 
84
84
  ```tsx
85
85
  <Button
86
- className={(state, defaultClassName) =>
87
- `${defaultClassName} ${state.isPressed ? 'pressed' : ''}`
86
+ className={(state, baseClassName) =>
87
+ `${baseClassName} ${state.isPressed ? 'pressed' : ''}`
88
88
  }
89
89
  >
90
90
  Press me
@@ -141,17 +141,17 @@ function useRender<T extends ElementType, S>(
141
141
 
142
142
  ```ts
143
143
  interface UseRenderOptions<T extends ElementType, S> {
144
- defaultProps?: React.ComponentProps<T>
144
+ baseProps?: React.ComponentProps<T>
145
145
  props?: ComponentProps<T, S>
146
146
  ref?: React.Ref<any> | (React.Ref<any> | undefined)[]
147
147
  }
148
148
  ```
149
149
 
150
- | Option | Description |
151
- | -------------- | ------------------------------------------------------------------ |
152
- | `defaultProps` | Default props applied to the element |
153
- | `props` | Consumer-provided props (typically forwarded from component props) |
154
- | `ref` | Ref(s) to merge with the consumer's ref |
150
+ | Option | Description |
151
+ | ----------- | ------------------------------------------------------------------ |
152
+ | `baseProps` | Base props applied to the element |
153
+ | `props` | Consumer-provided props (typically forwarded from component props) |
154
+ | `ref` | Ref(s) to merge with the consumer's ref |
155
155
 
156
156
  ### `ComponentProps<T, S>`
157
157
 
@@ -170,7 +170,7 @@ type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
170
170
 
171
171
  ```ts
172
172
  type ClassNameResolver<S> =
173
- | ((state: S, defaultClassName?: string) => string | undefined)
173
+ | ((state: S, baseClassName?: string) => string | undefined)
174
174
  | string
175
175
  | undefined
176
176
  ```
@@ -179,17 +179,17 @@ type ClassNameResolver<S> =
179
179
 
180
180
  ```ts
181
181
  type StyleResolver<S> =
182
- | ((state: S, defaultStyle?: CSSProperties) => CSSProperties | undefined)
182
+ | ((state: S, baseStyle?: CSSProperties) => CSSProperties | undefined)
183
183
  | CSSProperties
184
184
  | undefined
185
185
  ```
186
186
 
187
- ### `Renderer<T, S>`
187
+ ### `Renderer<S>`
188
188
 
189
189
  ```ts
190
- type Renderer<T extends ElementType, S> = (
191
- state: S,
190
+ type Renderer<S> = (
192
191
  props: ComponentPropsWithRef<T>,
192
+ state: S,
193
193
  ) => ReactNode
194
194
  ```
195
195
 
package/dist/types.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactNode } from 'react';
2
- export type ClassNameResolver<State> = ((state: State, defaultClassName?: string) => string | undefined) | string | undefined;
3
- export type StyleResolver<State> = ((state: State, defaultStyle?: CSSProperties) => CSSProperties | undefined) | CSSProperties | undefined;
4
- export type Renderer<S> = (state: S, props: React.HTMLAttributes<any> & {
2
+ export type ClassNameResolver<State> = ((state: State, baseClassName?: string) => string | undefined) | string | undefined;
3
+ export type StyleResolver<State> = ((state: State, baseStyle?: CSSProperties) => CSSProperties | undefined) | CSSProperties | undefined;
4
+ export type Renderer<S> = (props: React.HTMLAttributes<any> & {
5
5
  ref?: React.Ref<any> | undefined;
6
- }) => ReactNode;
6
+ }, state: S) => ReactNode;
7
7
  export type DataAttributes = Record<`data-${string}`, string | number | boolean>;
8
8
  export type BaseComponentProps<T extends ElementType> = Omit<ComponentPropsWithRef<T>, 'children' | 'className' | 'style'>;
@@ -7,12 +7,28 @@ export type ComponentProps<T extends ElementType, S> = BaseComponentProps<T> & {
7
7
  render?: Renderer<S> | JSX.Element;
8
8
  };
9
9
  export interface UseRenderOptions<T extends ElementType, S> {
10
- defaultProps?: React.ComponentProps<T> & DataAttributes;
10
+ baseProps?: React.ComponentProps<T> & DataAttributes;
11
11
  props?: ComponentProps<T, S> & DataAttributes;
12
12
  ref?: React.Ref<any> | (React.Ref<any> | undefined)[];
13
13
  }
14
14
  /**
15
- * Hook for enabling a render prop in custom components. Designed to be used by component libraries as an implementation detail
16
- * in providing a way to override a component's default rendered element.
15
+ * Hook for rendering elements with render prop support, prop merging, and state-driven className/style resolution.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { useRender, ComponentProps } from '@diskette/use-render'
20
+ *
21
+ * type State = { disabled: boolean; loading: boolean }
22
+ * type ButtonProps = ComponentProps<'button', State>
23
+ *
24
+ * function Button(props: ButtonProps) {
25
+ * const state: State = { disabled: props.disabled ?? false, loading: false }
26
+ * return useRender('button', state, { props })
27
+ * }
28
+ *
29
+ * // Usage:
30
+ * <Button className={(state) => state.disabled && 'opacity-50'} />
31
+ * <Button render={<a href="#" />} />
32
+ * ```
17
33
  */
18
34
  export declare function useRender<T extends ElementType, S>(tag: T, state: S, options: UseRenderOptions<T, S>): ReactNode;
@@ -2,24 +2,40 @@ import { cloneElement, createElement, isValidElement } from 'react';
2
2
  import { useComposedRef } from "./use-composed-ref.js";
3
3
  import { isFunction, isString, mergeProps, resolveClassName, resolveStyle, } from "./utils.js";
4
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.
5
+ * Hook for rendering elements with render prop support, prop merging, and state-driven className/style resolution.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useRender, ComponentProps } from '@diskette/use-render'
10
+ *
11
+ * type State = { disabled: boolean; loading: boolean }
12
+ * type ButtonProps = ComponentProps<'button', State>
13
+ *
14
+ * function Button(props: ButtonProps) {
15
+ * const state: State = { disabled: props.disabled ?? false, loading: false }
16
+ * return useRender('button', state, { props })
17
+ * }
18
+ *
19
+ * // Usage:
20
+ * <Button className={(state) => state.disabled && 'opacity-50'} />
21
+ * <Button render={<a href="#" />} />
22
+ * ```
7
23
  */
8
24
  export function useRender(tag, state, options) {
9
25
  // Workarounds for getting the prop objects to be typed. But should still be ok as the properties we need is common to all elements
10
- const defaultProps = options.defaultProps ?? {};
26
+ const baseProps = options.baseProps ?? {};
11
27
  const props = (options.props ?? {});
12
- const { className: defaultClassName, style: defaultStyle, children: defaultChildren, ...defaults } = defaultProps;
28
+ const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
13
29
  const { className, style, children, ref, render, ...rest } = props ?? {};
14
- const resolvedClassName = resolveClassName(defaultClassName, className, state);
15
- const resolvedStyle = resolveStyle(defaultStyle, style, state);
30
+ const resolvedClassName = resolveClassName(baseClassName, className, state);
31
+ const resolvedStyle = resolveStyle(baseStyle, style, state);
16
32
  const refs = Array.isArray(options.ref)
17
33
  ? [ref, ...options.ref]
18
34
  : [ref, options.ref];
19
35
  const mergedRef = useComposedRef(refs);
20
36
  // Another workaround for getting component props typed
21
37
  const resolvedProps = {
22
- ...mergeProps(defaults, rest),
38
+ ...mergeProps(base, rest),
23
39
  ref: mergedRef,
24
40
  };
25
41
  if (isString(resolvedClassName)) {
@@ -33,12 +49,12 @@ export function useRender(tag, state, options) {
33
49
  if (isValidElement(render)) {
34
50
  return cloneElement(render, resolvedProps, resolvedChildren);
35
51
  }
36
- // For `<Component render={(state, props) => <a {...props} />)} />`
52
+ // For `<Component render={(props) => <a {...props} />)} />`
37
53
  if (isFunction(render)) {
38
- return render(state, {
54
+ return render({
39
55
  ...resolvedProps,
40
56
  children: resolvedChildren,
41
- });
57
+ }, state);
42
58
  }
43
- return createElement(tag, resolvedProps, resolvedChildren ?? defaultChildren);
59
+ return createElement(tag, resolvedProps, resolvedChildren ?? baseChildren);
44
60
  }
@@ -8,7 +8,7 @@ function createBtn(defaultProps) {
8
8
  const state = { isActive };
9
9
  return useRender('button', state, {
10
10
  props,
11
- defaultProps: {
11
+ baseProps: {
12
12
  onClick: () => setIsActive((cur) => !cur),
13
13
  ...defaultProps,
14
14
  },
@@ -33,9 +33,9 @@ describe('useRender', () => {
33
33
  await button.click();
34
34
  await expect.element(button).toHaveClass('active');
35
35
  });
36
- test('className function receives defaultClassName as second argument', async () => {
36
+ test('className function receives baseClassName as second argument', async () => {
37
37
  const Button = createBtn({ className: 'btn-default' });
38
- const { getByRole } = await render(<Button className={(_state, defaultClassName) => `custom ${defaultClassName ?? ''}`}>
38
+ const { getByRole } = await render(<Button className={(_state, baseClassName) => `custom ${baseClassName ?? ''}`}>
39
39
  Click
40
40
  </Button>);
41
41
  const button = getByRole('button');
@@ -69,10 +69,10 @@ describe('useRender', () => {
69
69
  await button.click();
70
70
  await expect.element(button).toHaveStyle({ backgroundColor: 'green' });
71
71
  });
72
- test('style function receives defaultStyle as second argument', async () => {
72
+ test('style function receives baseStyle as second argument', async () => {
73
73
  const Button = createBtn({ style: { padding: '10px' } });
74
- const { getByRole } = await render(<Button style={(_state, defaultStyle) => ({
75
- ...defaultStyle,
74
+ const { getByRole } = await render(<Button style={(_state, baseStyle) => ({
75
+ ...baseStyle,
76
76
  color: 'blue',
77
77
  })}>
78
78
  Click
@@ -126,9 +126,9 @@ describe('useRender', () => {
126
126
  await expect.element(link).toHaveTextContent('Link');
127
127
  await expect.element(link).toHaveAttribute('href', '/path');
128
128
  });
129
- test('render function receives state and props', async () => {
129
+ test('render function receives props and state', async () => {
130
130
  const Button = createBtn();
131
- const { getByRole } = await render(<Button className="custom" render={(state, props) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
131
+ const { getByRole } = await render(<Button className="custom" render={(props, state) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
132
132
  e.preventDefault();
133
133
  props.onClick?.(e);
134
134
  }}/>)}>
@@ -153,7 +153,7 @@ describe('useRender', () => {
153
153
  });
154
154
  test('function render prop with children as function', async () => {
155
155
  const Button = createBtn();
156
- const { getByRole } = await render(<Button render={(state, props) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
156
+ const { getByRole } = await render(<Button render={(props, state) => (<a {...props} href="/path" data-active={state.isActive} onClick={(e) => {
157
157
  e.preventDefault();
158
158
  props.onClick?.(e);
159
159
  }}/>)}>
@@ -188,7 +188,7 @@ describe('useRender', () => {
188
188
  const [isActive, setIsActive] = useState(false);
189
189
  return useRender('button', { isActive }, {
190
190
  props,
191
- defaultProps: {
191
+ baseProps: {
192
192
  onClick: (_e) => {
193
193
  setIsActive((cur) => !cur);
194
194
  return defaultHandler();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diskette/use-render",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.5.0",
5
5
  "exports": "./dist/index.js",
6
6
  "files": [
7
7
  "dist"