Repository navigation
Expand file tree
/
Copy pathreact.ts
More file actions
131 lines (122 loc) · 4.28 KB
/
Copy pathreact.ts
File metadata and controls
131 lines (122 loc) · 4.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import {
createElement,
forwardRef,
useLayoutEffect,
useRef,
type ForwardRefExoticComponent,
type HTMLAttributes,
type MutableRefObject,
type Ref,
type RefAttributes
} from "react";
export interface ReactWrapperOptions {
displayName?: string;
events?: Record<string, string>;
}
function isPrimitive(value: unknown): boolean {
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function mergeRefs<T>(...refs: Array<Ref<T> | undefined>): (value: T | null) => void {
return (value) => {
for (const ref of refs) {
if (!ref) continue;
if (typeof ref === "function") {
ref(value);
} else {
(ref as MutableRefObject<T | null>).current = value;
}
}
};
}
export function wrapCustomElement<TElement extends HTMLElement = HTMLElement>(
tagName: string,
options: ReactWrapperOptions = {}
): ForwardRefExoticComponent<HTMLAttributes<TElement> & Record<string, any> & RefAttributes<TElement>> {
const Wrapped = forwardRef<TElement, HTMLAttributes<TElement> & Record<string, any>>((props, forwardedRef) => {
const localRef = useRef<TElement | null>(null);
const previousPropertyNames = useRef<Set<string>>(new Set());
const previousBooleanAttributes = useRef<Set<string>>(new Set());
const {
children,
style,
className,
dangerouslySetInnerHTML,
...rest
} = props;
const elementProps: Record<string, unknown> = {};
const attributeProps: Record<string, unknown> = {};
const booleanAttributes: Record<string, boolean> = {};
const eventProps = new Map<string, EventListener>();
Object.keys(rest).forEach((key) => {
const value = (rest as Record<string, unknown>)[key];
const explicitEvent = options.events?.[key];
if (explicitEvent && typeof value === "function") {
eventProps.set(explicitEvent, value as EventListener);
return;
}
if (/^on[A-Z]/.test(key) && typeof value === "function") {
eventProps.set(key.slice(2).toLowerCase(), value as EventListener);
return;
}
if (typeof value === "boolean") {
const attributeName = key === "className" ? "class" : key;
if (/^(aria-|data-)/.test(attributeName)) {
attributeProps[attributeName] = value;
} else {
elementProps[key] = value;
booleanAttributes[attributeName] = value;
}
return;
}
if (isPrimitive(value)) {
attributeProps[key === "className" ? "class" : key] = value;
} else {
elementProps[key] = value;
}
});
useLayoutEffect(() => {
const element = localRef.current;
if (!element) return undefined;
const nextPropertyNames = new Set(Object.keys(elementProps));
for (const key of Array.from(previousPropertyNames.current)) {
if (nextPropertyNames.has(key)) continue;
try {
(element as any)[key] = undefined;
} catch (_error) {
// Ignore non-writable host properties.
}
}
Object.keys(elementProps).forEach((key) => {
(element as any)[key] = elementProps[key];
});
previousPropertyNames.current = nextPropertyNames;
const nextBooleanAttributes = new Set(Object.keys(booleanAttributes));
for (const attributeName of Array.from(previousBooleanAttributes.current)) {
if (!nextBooleanAttributes.has(attributeName)) {
element.removeAttribute(attributeName);
}
}
Object.keys(booleanAttributes).forEach((attributeName) => {
element.toggleAttribute(attributeName, booleanAttributes[attributeName]);
});
previousBooleanAttributes.current = nextBooleanAttributes;
const cleanups: Array<() => void> = [];
for (const [eventName, listener] of eventProps) {
element.addEventListener(eventName, listener);
cleanups.push(() => element.removeEventListener(eventName, listener));
}
return () => {
cleanups.forEach((cleanup) => cleanup());
};
});
return createElement(tagName, {
...attributeProps,
className,
style,
dangerouslySetInnerHTML,
ref: mergeRefs(localRef, forwardedRef)
}, children);
});
Wrapped.displayName = options.displayName || `Ity(${tagName})`;
return Wrapped;
}