@diskette/use-render 0.3.0 → 0.4.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 +11 -11
- package/dist/types.d.ts +2 -2
- package/dist/use-render.d.ts +19 -3
- package/dist/use-render.js +24 -8
- package/dist/use-render.test.jsx +7 -7
- package/package.json +1 -1
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
|
-
|
|
42
|
+
baseProps: {
|
|
43
43
|
className: 'btn',
|
|
44
44
|
onMouseDown: () => setIsPressed(true),
|
|
45
45
|
onMouseUp: () => setIsPressed(false),
|
|
@@ -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,
|
|
87
|
-
`${
|
|
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
|
-
|
|
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
|
|
151
|
-
|
|
|
152
|
-
| `
|
|
153
|
-
| `props`
|
|
154
|
-
| `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,
|
|
173
|
+
| ((state: S, baseClassName?: string) => string | undefined)
|
|
174
174
|
| string
|
|
175
175
|
| undefined
|
|
176
176
|
```
|
|
@@ -179,7 +179,7 @@ type ClassNameResolver<S> =
|
|
|
179
179
|
|
|
180
180
|
```ts
|
|
181
181
|
type StyleResolver<S> =
|
|
182
|
-
| ((state: S,
|
|
182
|
+
| ((state: S, baseStyle?: CSSProperties) => CSSProperties | undefined)
|
|
183
183
|
| CSSProperties
|
|
184
184
|
| undefined
|
|
185
185
|
```
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, CSSProperties, ElementType, ReactNode } from 'react';
|
|
2
|
-
export type ClassNameResolver<State> = ((state: State,
|
|
3
|
-
export type StyleResolver<State> = ((state: State,
|
|
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
4
|
export type Renderer<S> = (state: S, props: React.HTMLAttributes<any> & {
|
|
5
5
|
ref?: React.Ref<any> | undefined;
|
|
6
6
|
}) => ReactNode;
|
package/dist/use-render.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
16
|
-
*
|
|
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;
|
package/dist/use-render.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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
|
|
26
|
+
const baseProps = options.baseProps ?? {};
|
|
11
27
|
const props = (options.props ?? {});
|
|
12
|
-
const { className:
|
|
28
|
+
const { className: baseClassName, style: baseStyle, children: baseChildren, ...base } = baseProps;
|
|
13
29
|
const { className, style, children, ref, render, ...rest } = props ?? {};
|
|
14
|
-
const resolvedClassName = resolveClassName(
|
|
15
|
-
const resolvedStyle = resolveStyle(
|
|
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(
|
|
38
|
+
...mergeProps(base, rest),
|
|
23
39
|
ref: mergedRef,
|
|
24
40
|
};
|
|
25
41
|
if (isString(resolvedClassName)) {
|
|
@@ -40,5 +56,5 @@ export function useRender(tag, state, options) {
|
|
|
40
56
|
children: resolvedChildren,
|
|
41
57
|
});
|
|
42
58
|
}
|
|
43
|
-
return createElement(tag, resolvedProps, resolvedChildren ??
|
|
59
|
+
return createElement(tag, resolvedProps, resolvedChildren ?? baseChildren);
|
|
44
60
|
}
|
package/dist/use-render.test.jsx
CHANGED
|
@@ -8,7 +8,7 @@ function createBtn(defaultProps) {
|
|
|
8
8
|
const state = { isActive };
|
|
9
9
|
return useRender('button', state, {
|
|
10
10
|
props,
|
|
11
|
-
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
75
|
-
...
|
|
74
|
+
const { getByRole } = await render(<Button style={(_state, baseStyle) => ({
|
|
75
|
+
...baseStyle,
|
|
76
76
|
color: 'blue',
|
|
77
77
|
})}>
|
|
78
78
|
Click
|
|
@@ -188,7 +188,7 @@ describe('useRender', () => {
|
|
|
188
188
|
const [isActive, setIsActive] = useState(false);
|
|
189
189
|
return useRender('button', { isActive }, {
|
|
190
190
|
props,
|
|
191
|
-
|
|
191
|
+
baseProps: {
|
|
192
192
|
onClick: (_e) => {
|
|
193
193
|
setIsActive((cur) => !cur);
|
|
194
194
|
return defaultHandler();
|