From 3c0e7b5b88fda6927bdfde50d9717fcba9d64198 Mon Sep 17 00:00:00 2001 From: Alex <8125011+alex-kinokon@users.noreply.github.com> Date: Tue, 14 Nov 2023 01:25:47 -0500 Subject: [PATCH] Fix #94 --- .eslintrc | 1 + .gitignore | 3 ++- CHANGELOG.md | 5 ++++ README.md | 4 +++ src/index.ts | 10 +++++++- src/jsx-dom.ts | 6 ++--- src/react-compat-api.ts | 14 +++++++++++ src/ref.ts | 4 +-- test/test-main.tsx | 56 +++++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 11 +++++++- 10 files changed, 106 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index 30e497f..dcab6a5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -61,6 +61,7 @@ "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/triple-slash-reference": "off", "@typescript-eslint/no-empty-interface": "off", "arrow-body-style": ["error", "as-needed"], diff --git a/.gitignore b/.gitignore index ec5ecbb..1625589 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ node_modules index.js build/* esm -cjs \ No newline at end of file +cjs +drafts diff --git a/CHANGELOG.md b/CHANGELOG.md index c2fb801..e11bc47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 8.1.0 +- Fixes #97: support `disabled` on `` element. +- Fixes #94: supports `forwardRef` and `useImperativeHandle`. +- Bumped TypeScript definition sync with `@types/react` at #e05c7e9. + # 8.0.5 - Added support for using `DOMTokenList` (e.g. `element.classList`) for `className`. - Renamed `ClassList` type declaration to `BasicClassList` to not confuse with the browser’s class list type. diff --git a/README.md b/README.md index 02fb81c..1660fcd 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,11 @@ The following functions are included for compatibility with React API: ```ts function createFactory(component: string): (props: object) => JSX.Element +function useImperativeHandle(ref: Ref, init: () => T, deps?: DependencyList): void function useRef(initialValue?: T): RefObject +function forwardRef( + render: (props: P, ref: Ref) => ReactNode +): FunctionComponent

}> ``` The following functions do **not** have memoization or optimization, and are only useful if you are diff --git a/src/index.ts b/src/index.ts index 114b940..d93d46c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,14 @@ export { createRef, isRef } from "./ref" export { useClassList, useText } from "./hooks" -export { memo, StrictMode, useCallback, useMemo, useRef } from "./react-compat-api" +export { + forwardRef, + memo, + StrictMode, + useCallback, + useImperativeHandle, + useMemo, + useRef, +} from "./react-compat-api" import { Component, Fragment, createElement } from "./jsx-dom" import { ShadowRoot } from "./shadow" diff --git a/src/jsx-dom.ts b/src/jsx-dom.ts index 846ed7d..df11b4d 100644 --- a/src/jsx-dom.ts +++ b/src/jsx-dom.ts @@ -196,8 +196,8 @@ export function createElement(tag: any, attr: any, ...children: any[]) { return jsx(tag, { ...attr, children }, attr.key) } -function attachRef(ref: any | undefined, node: Node) { - if (isRef(ref)) { +export function attachRef(ref: any | undefined, node: T) { + if (isRef(ref)) { ref.current = node } else if (isFunction(ref)) { ref(node) @@ -251,7 +251,7 @@ function style(node: Element & HTMLOrSVGElement, value?: any) { node.setAttribute("style", value) } else if (isObject(value)) { forEach(value, (val, key) => { - if (key.indexOf('-') === 0) { + if (key.indexOf("-") === 0) { // CSS custom properties (variables) start with `-` (e.g. `--my-variable`) // and must be assigned via `setProperty`. cast(node).style.setProperty(key, val) diff --git a/src/react-compat-api.ts b/src/react-compat-api.ts index c346da9..97d9ee6 100644 --- a/src/react-compat-api.ts +++ b/src/react-compat-api.ts @@ -1,3 +1,7 @@ +import type { FunctionComponent, ReactElement, Ref } from "../types" +import { attachRef } from "./jsx-dom" +import { createRef } from "./ref" + export { createRef as useRef } from "./ref" export { identity as useCallback, identity as memo } from "./util" export { Fragment as StrictMode } from "./jsx-dom" @@ -5,3 +9,13 @@ export { Fragment as StrictMode } from "./jsx-dom" export function useMemo(factory: () => T): T { return factory() } + +export function forwardRef( + render: (props: P, ref: Ref) => ReactElement +): FunctionComponent

}> { + return ({ ref, ...props }) => render(props as P, ref ?? createRef()) +} + +export function useImperativeHandle(ref: Ref, init: () => T, _deps: unknown) { + attachRef(ref, init()) +} diff --git a/src/ref.ts b/src/ref.ts index 681a17b..8bde80c 100644 --- a/src/ref.ts +++ b/src/ref.ts @@ -1,6 +1,6 @@ import { isObject } from "./util" -interface Ref { +interface Ref { current: null | T } @@ -8,6 +8,6 @@ export function createRef(): Ref { return Object.seal({ current: null }) } -export function isRef(maybeRef: any): maybeRef is Ref { +export function isRef(maybeRef: any): maybeRef is Ref { return isObject(maybeRef) && "current" in maybeRef } diff --git a/test/test-main.tsx b/test/test-main.tsx index 26d9207..6469fe0 100644 --- a/test/test-main.tsx +++ b/test/test-main.tsx @@ -321,6 +321,62 @@ describe("jsx-dom", () => { expect(button).to.be.instanceOf(HTMLButtonElement) }) + describe("supports forwardRef", () => { + it("element", () => { + const Container = React.forwardRef((props, ref) => ( +

+ +
+ )) + + const ref = lib.createRef() + const node = ( + + Click me! + + ) + expect(node.className).to.equal("container") + expect(ref.current).to.be.instanceOf(HTMLButtonElement) + }) + + it("component", () => { + const Button = props => + + )) + + const ref = lib.createRef() + const node = ( + + Click me! + + ) + expect(node.className).to.equal("container") + expect(ref.current).to.be.instanceOf(HTMLButtonElement) + }) + }) + + it("supports useImperativeHandle", () => { + const Button = React.forwardRef<{ focus: () => string }, any>((props, ref) => { + React.useImperativeHandle(ref, () => ({ + focus: () => "ping", + })) + return + ) + expect(node.className).to.equal("container") + expect(ref.current).to.have.property("focus") + expect(ref.current.focus()).to.equal("ping") + }) + it("supports defaultProps in functional components", () => { const Button = (props: any) =>
Button.defaultProps = { className: "defaultClass" } diff --git a/types/index.d.ts b/types/index.d.ts index 33758e2..38d92d3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -58,7 +58,7 @@ interface AttrWithRef extends Attributes { ref?: Ref | undefined } -type ReactElement = HTMLElement | SVGElement +export type ReactElement = HTMLElement | SVGElement type DOMFactory

, T extends Element> = ( props?: (AttrWithRef & P) | null, @@ -255,6 +255,13 @@ export interface MutableRefObject { export function createRef(): RefObject +/** + * React compatibility-only API. + */ +export function forwardRef( + render: (props: P, ref: Ref) => ReactNode +): FunctionComponent

}> + /** * `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument * (`initialValue`). The returned object will persist for the full lifetime of the component. @@ -297,6 +304,8 @@ export function useRef(initialValue: T | null): RefObject */ export function useRef(): MutableRefObject +export function useImperativeHandle(ref: Ref, init: () => T, deps?: DependencyList): void + // I made 'inputs' required here and in useMemo as there's no point to memoizing without the memoization key // useCallback(X) is identical to just using X, useMemo(() => Y) is identical to just using Y. /**