(fn: (a: A) => R, a: A): R;
- export function unstable_batchedUpdates(
- callback: (arg?: any) => void,
- arg?: any
- ): void;
-
export type PropsWithChildren = P & {
- children?: preact.ComponentChildren | undefined;
+ children?: preact1.ComponentChildren | undefined;
};
export const Children: {
- map(
+ map(
children: T | T[],
fn: (child: T, i: number) => R
): R[];
- forEach(
+ forEach(
children: T | T[],
fn: (child: T, i: number) => void
): void;
- count: (children: preact.ComponentChildren) => number;
- only: (children: preact.ComponentChildren) => preact.ComponentChild;
- toArray: (children: preact.ComponentChildren) => preact.VNode<{}>[];
+ count: (children: preact1.ComponentChildren) => number;
+ only: (children: preact1.ComponentChildren) => preact1.ComponentChild;
+ toArray: (children: preact1.ComponentChildren) => preact1.VNode<{}>[];
};
// scheduler
diff --git a/compat/src/index.js b/compat/src/index.js
index 61fd2f3625..d609365ea9 100644
--- a/compat/src/index.js
+++ b/compat/src/index.js
@@ -32,7 +32,6 @@ import { memo } from './memo';
import { forwardRef } from './forwardRef';
import { Children } from './Children';
import { Suspense, lazy } from './suspense';
-import { SuspenseList } from './suspense-list';
import { createPortal } from './portals';
import {
hydrate,
@@ -117,21 +116,12 @@ function unmountComponentAtNode(container) {
function findDOMNode(component) {
return (
(component &&
- (component.base || (component.nodeType === 1 && component))) ||
+ ((component._vnode && component._vnode._dom) ||
+ (component.nodeType === 1 && component))) ||
null
);
}
-/**
- * Deprecated way to control batched rendering inside the reconciler, but we
- * already schedule in batches inside our rendering code
- * @template Arg
- * @param {(arg: Arg) => void} callback function that triggers the updated
- * @param {Arg} [arg] Optional argument that can be passed to the callback
- */
-// eslint-disable-next-line camelcase
-const unstable_batchedUpdates = (callback, arg) => callback(arg);
-
/**
* In React, `flushSync` flushes the entire tree and forces a rerender. It's
* implmented here as a no-op.
@@ -180,11 +170,8 @@ export {
useDeferredValue,
useSyncExternalStore,
useTransition,
- // eslint-disable-next-line camelcase
- unstable_batchedUpdates,
StrictMode,
Suspense,
- SuspenseList,
lazy,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
};
@@ -229,10 +216,8 @@ export default {
memo,
forwardRef,
flushSync,
- unstable_batchedUpdates,
StrictMode,
Suspense,
- SuspenseList,
lazy,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
};
diff --git a/compat/src/internal.d.ts b/compat/src/internal.d.ts
index efc5287ca3..31795ba123 100644
--- a/compat/src/internal.d.ts
+++ b/compat/src/internal.d.ts
@@ -27,12 +27,11 @@ export interface Component extends PreactComponent
{
export interface FunctionComponent
extends PreactFunctionComponent
{
shouldComponentUpdate?(nextProps: Readonly
): boolean;
- _forwarded?: boolean;
_patchedLifecycles?: true;
}
export interface VNode extends PreactVNode {
- $$typeof?: symbol | string;
+ $$typeof?: symbol;
preactCompatNormalized?: boolean;
}
diff --git a/compat/src/memo.js b/compat/src/memo.js
index e743199055..925e0c9eae 100644
--- a/compat/src/memo.js
+++ b/compat/src/memo.js
@@ -29,6 +29,5 @@ export function memo(c, comparer) {
}
Memoed.displayName = 'Memo(' + (c.displayName || c.name) + ')';
Memoed.prototype.isReactComponent = true;
- Memoed._forwarded = true;
return Memoed;
}
diff --git a/compat/src/portals.js b/compat/src/portals.js
index 2e126f9076..c082e58364 100644
--- a/compat/src/portals.js
+++ b/compat/src/portals.js
@@ -46,14 +46,9 @@ function Portal(props) {
parentNode: container,
childNodes: [],
_children: { _mask: root._mask },
- contains: () => true,
insertBefore(child, before) {
this.childNodes.push(child);
_this._container.insertBefore(child, before);
- },
- removeChild(child) {
- this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1);
- _this._container.removeChild(child);
}
};
}
diff --git a/compat/src/render.js b/compat/src/render.js
index f18cbd896b..9807c0fc0b 100644
--- a/compat/src/render.js
+++ b/compat/src/render.js
@@ -24,10 +24,9 @@ import {
useSyncExternalStore,
useTransition
} from './index';
+import { assign, IS_NON_DIMENSIONAL } from './util';
-export const REACT_ELEMENT_TYPE =
- (typeof Symbol != 'undefined' && Symbol.for && Symbol.for('react.element')) ||
- 0xeac7;
+export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
const CAMEL_PROPS =
/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
@@ -36,44 +35,11 @@ const CAMEL_REPLACE = /[A-Z0-9]/g;
const IS_DOM = typeof document !== 'undefined';
// Input types for which onchange should not be converted to oninput.
-// type="file|checkbox|radio", plus "range" in IE11.
-// (IE11 doesn't support Symbol, which we use here to turn `rad` into `ra` which matches "range")
-const onChangeInputType = type =>
- (typeof Symbol != 'undefined' && typeof Symbol() == 'symbol'
- ? /fil|che|rad/
- : /fil|che|ra/
- ).test(type);
+const onChangeInputType = type => /fil|che|rad/.test(type);
// Some libraries like `react-virtualized` explicitly check for this.
Component.prototype.isReactComponent = {};
-// `UNSAFE_*` lifecycle hooks
-// Preact only ever invokes the unprefixed methods.
-// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method.
-// - If a component defines its own `componentDidMount()` (including via defineProperty), use that.
-// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter.
-// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property.
-// See https://github.com/preactjs/preact/issues/1941
-[
- 'componentWillMount',
- 'componentWillReceiveProps',
- 'componentWillUpdate'
-].forEach(key => {
- Object.defineProperty(Component.prototype, key, {
- configurable: true,
- get() {
- return this['UNSAFE_' + key];
- },
- set(v) {
- Object.defineProperty(this, key, {
- configurable: true,
- writable: true,
- value: v
- });
- }
- });
-});
-
/**
* Proxy render() since React returns a Component reference.
* @param {import('./internal').VNode} vnode VNode tree to render
@@ -151,7 +117,17 @@ function handleDomVNode(vnode) {
}
let lowerCased = i.toLowerCase();
- if (i === 'defaultValue' && 'value' in props && props.value == null) {
+ if (i === 'style' && typeof value === 'object') {
+ for (let key in value) {
+ if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) {
+ value[key] += 'px';
+ }
+ }
+ } else if (
+ i === 'defaultValue' &&
+ 'value' in props &&
+ props.value == null
+ ) {
// `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined.
// `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property.
i = 'value';
@@ -245,8 +221,15 @@ options.vnode = vnode => {
// only normalize props on Element nodes
if (typeof vnode.type === 'string') {
handleDomVNode(vnode);
+ } else if (typeof vnode.type === 'function' && vnode.type.defaultProps) {
+ let normalizedProps = assign({}, vnode.props);
+ for (let i in vnode.type.defaultProps) {
+ if (normalizedProps[i] === undefined) {
+ normalizedProps[i] = vnode.type.defaultProps[i];
+ }
+ }
+ vnode.props = normalizedProps;
}
-
vnode.$$typeof = REACT_ELEMENT_TYPE;
if (oldVNodeHook) oldVNodeHook(vnode);
diff --git a/compat/src/suspense-list.d.ts b/compat/src/suspense-list.d.ts
deleted file mode 100644
index 0a3be0adc9..0000000000
--- a/compat/src/suspense-list.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Intentionally not using a relative path to take advantage of
-// the TS version resolution mechanism
-import { Component, ComponentChild, ComponentChildren } from 'preact';
-
-//
-// SuspenseList
-// -----------------------------------
-
-export interface SuspenseListProps {
- children?: ComponentChildren;
- revealOrder?: 'forwards' | 'backwards' | 'together';
-}
-
-export class SuspenseList extends Component {
- render(): ComponentChild;
-}
diff --git a/compat/src/suspense-list.js b/compat/src/suspense-list.js
deleted file mode 100644
index 5e5d750a08..0000000000
--- a/compat/src/suspense-list.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import { Component, toChildArray } from 'preact';
-import { suspended } from './suspense.js';
-
-// Indexes to linked list nodes (nodes are stored as arrays to save bytes).
-const SUSPENDED_COUNT = 0;
-const RESOLVED_COUNT = 1;
-const NEXT_NODE = 2;
-
-// Having custom inheritance instead of a class here saves a lot of bytes.
-export function SuspenseList() {
- this._next = null;
- this._map = null;
-}
-
-// Mark one of child's earlier suspensions as resolved.
-// Some pending callbacks may become callable due to this
-// (e.g. the last suspended descendant gets resolved when
-// revealOrder === 'together'). Process those callbacks as well.
-const resolve = (list, child, node) => {
- if (++node[RESOLVED_COUNT] === node[SUSPENDED_COUNT]) {
- // The number a child (or any of its descendants) has been suspended
- // matches the number of times it's been resolved. Therefore we
- // mark the child as completely resolved by deleting it from ._map.
- // This is used to figure out when *all* children have been completely
- // resolved when revealOrder is 'together'.
- list._map.delete(child);
- }
-
- // If revealOrder is falsy then we can do an early exit, as the
- // callbacks won't get queued in the node anyway.
- // If revealOrder is 'together' then also do an early exit
- // if all suspended descendants have not yet been resolved.
- if (
- !list.props.revealOrder ||
- (list.props.revealOrder[0] === 't' && list._map.size)
- ) {
- return;
- }
-
- // Walk the currently suspended children in order, calling their
- // stored callbacks on the way. Stop if we encounter a child that
- // has not been completely resolved yet.
- node = list._next;
- while (node) {
- while (node.length > 3) {
- node.pop()();
- }
- if (node[RESOLVED_COUNT] < node[SUSPENDED_COUNT]) {
- break;
- }
- list._next = node = node[NEXT_NODE];
- }
-};
-
-// Things we do here to save some bytes but are not proper JS inheritance:
-// - call `new Component()` as the prototype
-// - do not set `Suspense.prototype.constructor` to `Suspense`
-SuspenseList.prototype = new Component();
-
-SuspenseList.prototype._suspended = function (child) {
- const list = this;
- const delegated = suspended(list._vnode);
-
- let node = list._map.get(child);
- node[SUSPENDED_COUNT]++;
-
- return unsuspend => {
- const wrappedUnsuspend = () => {
- if (!list.props.revealOrder) {
- // Special case the undefined (falsy) revealOrder, as there
- // is no need to coordinate a specific order or unsuspends.
- unsuspend();
- } else {
- node.push(unsuspend);
- resolve(list, child, node);
- }
- };
- if (delegated) {
- delegated(wrappedUnsuspend);
- } else {
- wrappedUnsuspend();
- }
- };
-};
-
-SuspenseList.prototype.render = function (props) {
- this._next = null;
- this._map = new Map();
-
- const children = toChildArray(props.children);
- if (props.revealOrder && props.revealOrder[0] === 'b') {
- // If order === 'backwards' (or, well, anything starting with a 'b')
- // then flip the child list around so that the last child will be
- // the first in the linked list.
- children.reverse();
- }
- // Build the linked list. Iterate through the children in reverse order
- // so that `_next` points to the first linked list node to be resolved.
- for (let i = children.length; i--; ) {
- // Create a new linked list node as an array of form:
- // [suspended_count, resolved_count, next_node]
- // where suspended_count and resolved_count are numeric counters for
- // keeping track how many times a node has been suspended and resolved.
- //
- // Note that suspended_count starts from 1 instead of 0, so we can block
- // processing callbacks until componentDidMount has been called. In a sense
- // node is suspended at least until componentDidMount gets called!
- //
- // Pending callbacks are added to the end of the node:
- // [suspended_count, resolved_count, next_node, callback_0, callback_1, ...]
- this._map.set(children[i], (this._next = [1, 0, this._next]));
- }
- return props.children;
-};
-
-SuspenseList.prototype.componentDidUpdate =
- SuspenseList.prototype.componentDidMount = function () {
- // Iterate through all children after mounting for two reasons:
- // 1. As each node[SUSPENDED_COUNT] starts from 1, this iteration increases
- // each node[RELEASED_COUNT] by 1, therefore balancing the counters.
- // The nodes can now be completely consumed from the linked list.
- // 2. Handle nodes that might have gotten resolved between render and
- // componentDidMount.
- this._map.forEach((node, child) => {
- resolve(this, child, node);
- });
- };
diff --git a/compat/src/suspense.js b/compat/src/suspense.js
index 3bb60c9eb0..1e5bd55fc6 100644
--- a/compat/src/suspense.js
+++ b/compat/src/suspense.js
@@ -125,8 +125,6 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
}
c._suspenders.push(suspendingComponent);
- const resolve = suspended(c._vnode);
-
let resolved = false;
const onResolved = () => {
if (resolved) return;
@@ -134,11 +132,7 @@ Suspense.prototype._childDidSuspend = function (promise, suspendingVNode) {
resolved = true;
suspendingComponent._onResolve = null;
- if (resolve) {
- resolve(onSuspensionComplete);
- } else {
- onSuspensionComplete();
- }
+ onSuspensionComplete();
};
suspendingComponent._onResolve = onResolved;
@@ -218,29 +212,6 @@ Suspense.prototype.render = function (props, state) {
];
};
-/**
- * Checks and calls the parent component's _suspended method, passing in the
- * suspended vnode. This is a way for a parent (e.g. SuspenseList) to get notified
- * that one of its children/descendants suspended.
- *
- * The parent MAY return a callback. The callback will get called when the
- * suspension resolves, notifying the parent of the fact.
- * Moreover, the callback gets function `unsuspend` as a parameter. The resolved
- * child descendant will not actually get unsuspended until `unsuspend` gets called.
- * This is a way for the parent to delay unsuspending.
- *
- * If the parent does not return a callback then the resolved vnode
- * gets unsuspended immediately when it resolves.
- *
- * @param {import('./internal').VNode} vnode
- * @returns {((unsuspend: () => void) => void)?}
- */
-export function suspended(vnode) {
- /** @type {import('./internal').Component} */
- let component = vnode._parent._component;
- return component && component._suspended && component._suspended(vnode);
-}
-
export function lazy(loader) {
let prom;
let component;
@@ -271,6 +242,5 @@ export function lazy(loader) {
}
Lazy.displayName = 'Lazy';
- Lazy._forwarded = true;
return Lazy;
}
diff --git a/compat/src/util.js b/compat/src/util.js
index 8ec376942b..43c66639a0 100644
--- a/compat/src/util.js
+++ b/compat/src/util.js
@@ -1,14 +1,4 @@
-/**
- * Assign properties from `props` to `obj`
- * @template O, P The obj and props types
- * @param {O} obj The object to copy properties to
- * @param {P} props The object to copy properties from
- * @returns {O & P}
- */
-export function assign(obj, props) {
- for (let i in props) obj[i] = props[i];
- return /** @type {O & P} */ (obj);
-}
+export const assign = Object.assign;
/**
* Check if two objects have a different shape
@@ -22,12 +12,5 @@ export function shallowDiffers(a, b) {
return false;
}
-/**
- * Check if two values are the same value
- * @param {*} x
- * @param {*} y
- * @returns {boolean}
- */
-export function is(x, y) {
- return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
-}
+export const IS_NON_DIMENSIONAL =
+ /^(-|f[lo].*[^se]$|g.{5,}[^ps]$|z|o[pr]|(W.{5})?[lL]i.*(t|mp)$|an|(bo|s).{4}Im|sca|m.{6}[ds]|ta|c.*[st]$|wido|ini)/;
diff --git a/compat/test/browser/component.test.js b/compat/test/browser/component.test.js
index de78a1fd20..9aecf754bc 100644
--- a/compat/test/browser/component.test.js
+++ b/compat/test/browser/component.test.js
@@ -75,169 +75,4 @@ describe('components', () => {
children: 'second'
});
});
-
- describe('UNSAFE_* lifecycle methods', () => {
- it('should support UNSAFE_componentWillMount', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillMount() {
- spy();
- }
-
- render() {
- return foo ;
- }
- }
-
- React.render( , scratch);
-
- expect(spy).to.be.calledOnce;
- });
-
- it('should support UNSAFE_componentWillMount #2', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- render() {
- return foo ;
- }
- }
-
- Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillMount', {
- value: spy
- });
-
- React.render( , scratch);
- expect(spy).to.be.calledOnce;
- });
-
- it('should support UNSAFE_componentWillReceiveProps', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps() {
- spy();
- }
-
- render() {
- return foo ;
- }
- }
-
- React.render( , scratch);
- // Trigger an update
- React.render( , scratch);
- expect(spy).to.be.calledOnce;
- });
-
- it('should support UNSAFE_componentWillReceiveProps #2', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- render() {
- return foo ;
- }
- }
-
- Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillReceiveProps', {
- value: spy
- });
-
- React.render( , scratch);
- // Trigger an update
- React.render( , scratch);
- expect(spy).to.be.calledOnce;
- });
-
- it('should support UNSAFE_componentWillUpdate', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillUpdate() {
- spy();
- }
-
- render() {
- return foo ;
- }
- }
-
- React.render( , scratch);
- // Trigger an update
- React.render( , scratch);
- expect(spy).to.be.calledOnce;
- });
-
- it('should support UNSAFE_componentWillUpdate #2', () => {
- let spy = sinon.spy();
-
- class Foo extends React.Component {
- render() {
- return foo ;
- }
- }
-
- Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillUpdate', {
- value: spy
- });
-
- React.render( , scratch);
- // Trigger an update
- React.render( , scratch);
- expect(spy).to.be.calledOnce;
- });
-
- it('should alias UNSAFE_* method to non-prefixed variant', () => {
- let inst;
- class Foo extends React.Component {
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillMount() {}
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillReceiveProps() {}
- // eslint-disable-next-line camelcase
- UNSAFE_componentWillUpdate() {}
- render() {
- inst = this;
- return foo
;
- }
- }
-
- React.render( , scratch);
-
- expect(inst.UNSAFE_componentWillMount).to.equal(inst.componentWillMount);
- expect(inst.UNSAFE_componentWillReceiveProps).to.equal(
- inst.UNSAFE_componentWillReceiveProps
- );
- expect(inst.UNSAFE_componentWillUpdate).to.equal(
- inst.UNSAFE_componentWillUpdate
- );
- });
-
- it('should call UNSAFE_* methods through Suspense with wrapper component #2525', () => {
- class Page extends React.Component {
- UNSAFE_componentWillMount() {}
- render() {
- return Example ;
- }
- }
-
- const Wrapper = () => ;
-
- sinon.spy(Page.prototype, 'UNSAFE_componentWillMount');
-
- React.render(
- fallback}>
-
- ,
- scratch
- );
-
- expect(scratch.innerHTML).to.equal('Example ');
- expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called;
- });
- });
});
diff --git a/compat/test/browser/events.test.js b/compat/test/browser/events.test.js
index d59dbef8ed..589dd46f59 100644
--- a/compat/test/browser/events.test.js
+++ b/compat/test/browser/events.test.js
@@ -30,10 +30,6 @@ describe('preact/compat events', () => {
it('should patch events', () => {
let spy = sinon.spy(event => {
- // Calling ev.preventDefault() outside of an event handler
- // does nothing in IE11. So we move these asserts inside
- // the event handler. We ensure that it's called once
- // in another assertion
expect(event.isDefaultPrevented()).to.be.false;
event.preventDefault();
expect(event.isDefaultPrevented()).to.be.true;
@@ -89,49 +85,16 @@ describe('preact/compat events', () => {
expect(vnode.props).to.not.haveOwnProperty('oninputCapture');
});
- it('should normalize onChange for range, except in IE11', () => {
- // NOTE: we don't normalize `onchange` for range inputs in IE11.
- const eventType = /Trident\//.test(navigator.userAgent)
- ? 'change'
- : 'input';
-
+ it('should normalize onChange for range', () => {
render( null} />, scratch);
expect(proto.addEventListener).to.have.been.calledOnce;
expect(proto.addEventListener).to.have.been.calledWithExactly(
- eventType,
+ 'input',
sinon.match.func,
false
);
});
- it('should normalize onChange for range, except in IE11, including when IE11 has Symbol polyfill', () => {
- // NOTE: we don't normalize `onchange` for range inputs in IE11.
- // This test mimics a specific scenario when a Symbol polyfill may
- // be present, in which case onChange should still not be normalized
-
- const isIE11 = /Trident\//.test(navigator.userAgent);
- const eventType = isIE11 ? 'change' : 'input';
-
- if (isIE11) {
- window.Symbol = () => 'mockSymbolPolyfill';
- }
- sinon.spy(window, 'Symbol');
-
- render( null} />, scratch);
- expect(window.Symbol).to.have.been.calledOnce;
- expect(proto.addEventListener).to.have.been.calledOnce;
- expect(proto.addEventListener).to.have.been.calledWithExactly(
- eventType,
- sinon.match.func,
- false
- );
-
- window.Symbol.restore();
- if (isIE11) {
- window.Symbol = undefined;
- }
- });
-
it('should support onAnimationEnd', () => {
const func = sinon.spy(() => {});
render(
, scratch);
diff --git a/compat/test/browser/exports.test.js b/compat/test/browser/exports.test.js
index cced23da45..d96e4bed4c 100644
--- a/compat/test/browser/exports.test.js
+++ b/compat/test/browser/exports.test.js
@@ -58,7 +58,6 @@ describe('compat exports', () => {
expect(Compat.Children.toArray).to.exist.and.be.a('function');
expect(Compat.Children.only).to.exist.and.be.a('function');
expect(Compat.unmountComponentAtNode).to.exist.and.be.a('function');
- expect(Compat.unstable_batchedUpdates).to.exist.and.be.a('function');
expect(Compat.version).to.exist.and.be.a('string');
expect(Compat.startTransition).to.be.a('function');
});
@@ -99,7 +98,6 @@ describe('compat exports', () => {
expect(Named.Children.toArray).to.exist.and.be.a('function');
expect(Named.Children.only).to.exist.and.be.a('function');
expect(Named.unmountComponentAtNode).to.exist.and.be.a('function');
- expect(Named.unstable_batchedUpdates).to.exist.and.be.a('function');
expect(Named.version).to.exist.and.be.a('string');
});
});
diff --git a/compat/test/browser/forwardRef.test.js b/compat/test/browser/forwardRef.test.js
index f69d5ae014..4e7968dd6d 100644
--- a/compat/test/browser/forwardRef.test.js
+++ b/compat/test/browser/forwardRef.test.js
@@ -35,7 +35,7 @@ describe('forwardRef', () => {
expect(App.prototype.isReactComponent).to.equal(true);
});
- it('should have $$typeof property', () => {
+ it.skip('should have $$typeof property', () => {
let App = forwardRef((_, ref) => foo
);
const expected = getSymbol('react.forward_ref', 0xf47);
expect(App.$$typeof).to.equal(expected);
@@ -402,8 +402,7 @@ describe('forwardRef', () => {
const Transition = ({ children }) => {
const state = useState(0);
forceTransition = state[1];
- expect(children.ref).to.not.be.undefined;
- if (state[0] === 0) expect(children.props.ref).to.be.undefined;
+ expect(children.props.ref).to.not.be.undefined;
return children;
};
diff --git a/compat/test/browser/memo.test.js b/compat/test/browser/memo.test.js
index 0cbd6fe8cc..89063209e7 100644
--- a/compat/test/browser/memo.test.js
+++ b/compat/test/browser/memo.test.js
@@ -72,9 +72,9 @@ describe('memo()', () => {
let ref = null;
- function Foo() {
+ function Foo(props) {
spy();
- return Hello World ;
+ return Hello World ;
}
let Memoized = memo(Foo);
@@ -99,8 +99,7 @@ describe('memo()', () => {
update();
rerender();
- expect(ref.current).not.to.be.undefined;
-
+ expect(ref.current).to.equal(scratch.firstChild);
// TODO: not sure whether this is in-line with react...
expect(spy).to.be.calledTwice;
});
@@ -175,8 +174,8 @@ describe('memo()', () => {
it('should pass ref through nested memos', () => {
class Foo extends Component {
- render() {
- return Hello World ;
+ render(props) {
+ return Hello World ;
}
}
@@ -187,7 +186,7 @@ describe('memo()', () => {
render( , scratch);
expect(ref.current).not.to.be.undefined;
- expect(ref.current).to.be.instanceOf(Foo);
+ expect(ref.current).to.equal(scratch.firstChild);
});
it('should not unnecessarily reorder children #2895', () => {
diff --git a/compat/test/browser/render.test.js b/compat/test/browser/render.test.js
index 7781acb6d8..6de00ac514 100644
--- a/compat/test/browser/render.test.js
+++ b/compat/test/browser/render.test.js
@@ -159,6 +159,85 @@ describe('compat render', () => {
expect(scratch.firstElementChild.value).to.equal('0');
});
+ it('should use defaultProps when prop is undefined', () => {
+ function TestComponent({ message }) {
+ return {message}
;
+ }
+ TestComponent.defaultProps = {
+ message: 'default message'
+ };
+
+ render( , scratch);
+ expect(scratch.textContent).to.equal('default message');
+ });
+
+ it('should not use defaultProps when prop is null', () => {
+ function TestComponent({ message }) {
+ return {message === null ? 'null value' : message}
;
+ }
+ TestComponent.defaultProps = {
+ message: 'default message'
+ };
+
+ render( , scratch);
+ expect(scratch.textContent).to.equal('null value');
+ });
+
+ it('should not use defaultProps when prop has a value', () => {
+ function TestComponent({ message }) {
+ return {message}
;
+ }
+ TestComponent.defaultProps = {
+ message: 'default message'
+ };
+
+ render( , scratch);
+ expect(scratch.textContent).to.equal('actual message');
+ });
+
+ it('should use defaultProps when prop is missing entirely', () => {
+ function TestComponent({ message }) {
+ return {message}
;
+ }
+ TestComponent.defaultProps = {
+ message: 'default message'
+ };
+
+ render( , scratch);
+ expect(scratch.textContent).to.equal('default message');
+ });
+
+ it('should handle multiple defaultProps with mixed prop states', () => {
+ function TestComponent({ title, message, count }) {
+ return (
+
+
{title}
+
{message}
+
{count}
+
+ );
+ }
+ TestComponent.defaultProps = {
+ title: 'Default Title',
+ message: 'Default Message',
+ count: 42
+ };
+
+ // title is undefined (should use default), message is null (should not use default), count has value (should not use default)
+ render(
+ ,
+ scratch
+ );
+
+ const title = scratch.querySelector('h1');
+ const message = scratch.querySelector('p');
+ const count = scratch.querySelector('span');
+
+ expect(title.textContent).to.equal('Default Title');
+ expect(message.textContent).to.equal(''); // null renders as empty
+ expect(count.textContent).to.equal('0'); // 0 is a valid value
+ });
+
it('should call onChange and onInput when input event is dispatched', () => {
const onChange = sinon.spy();
const onInput = sinon.spy();
@@ -264,12 +343,7 @@ describe('compat render', () => {
scratch
);
- let html = sortAttributes(scratch.innerHTML);
- if (/Trident/.test(navigator.userAgent)) {
- html = html.toLowerCase();
- }
-
- expect(html).to.equal(
+ expect(sortAttributes(scratch.innerHTML)).to.equal(
' '
);
});
@@ -597,4 +671,214 @@ describe('compat render', () => {
expect(scratch.textContent).to.equal('foo');
});
+
+ it('should append "px" to unitless inline css values', () => {
+ // These are all CSS Properties that support a single value
+ // that must have a unit. If we encounter a number we append "px" to it.
+ // The list is taken from: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference
+ const unitless = {
+ 'border-block': 2,
+ 'border-block-end-width': 3,
+ 'border-block-start-width': 4,
+ 'border-block-width': 5,
+ 'border-bottom-left-radius': 6,
+ 'border-bottom-right-radius': 7,
+ 'border-bottom-width': 8,
+ 'border-end-end-radius': 9,
+ 'border-end-start-radius': 10,
+ 'border-image-outset': 11,
+ 'border-image-width': 12,
+ 'border-inline': 2,
+ 'border-inline-end': 3,
+ 'border-inline-end-width': 4,
+ 'border-inline-start': 1,
+ 'border-inline-start-width': 123,
+ 'border-inline-width': 123,
+ 'border-left': 123,
+ 'border-left-width': 123,
+ 'border-radius': 123,
+ 'border-right': 123,
+ 'border-right-width': 123,
+ 'border-spacing': 123,
+ 'border-start-end-radius': 123,
+ 'border-start-start-radius': 123,
+ 'border-top': 123,
+ 'border-top-left-radius': 123,
+ 'border-top-right-radius': 123,
+ 'border-top-width': 123,
+ 'border-width': 123,
+ bottom: 123,
+ 'column-gap': 123,
+ 'column-rule-width': 23,
+ 'column-width': 23,
+ 'flex-basis': 23,
+ 'font-size': 123,
+ 'grid-gap': 23,
+ 'grid-auto-columns': 123,
+ 'grid-auto-rows': 123,
+ 'grid-template-columns': 23,
+ 'grid-template-rows': 23,
+ height: 123,
+ 'inline-size': 23,
+ inset: 23,
+ 'inset-block-end': 12,
+ 'inset-block-start': 12,
+ 'inset-inline-end': 213,
+ 'inset-inline-start': 213,
+ left: 213,
+ 'letter-spacing': 213,
+ margin: 213,
+ 'margin-block': 213,
+ 'margin-block-end': 213,
+ 'margin-block-start': 213,
+ 'margin-bottom': 213,
+ 'margin-inline': 213,
+ 'margin-inline-end': 213,
+ 'margin-inline-start': 213,
+ 'margin-left': 213,
+ 'margin-right': 213,
+ 'margin-top': 213,
+ 'mask-position': 23,
+ 'mask-size': 23,
+ 'max-block-size': 23,
+ 'max-height': 23,
+ 'max-inline-size': 23,
+ 'max-width': 23,
+ 'min-block-size': 23,
+ 'min-height': 23,
+ 'min-inline-size': 23,
+ 'min-width': 23,
+ 'object-position': 23,
+ 'outline-offset': 23,
+ 'outline-width': 123,
+ padding: 123,
+ 'padding-block': 123,
+ 'padding-block-end': 123,
+ 'padding-block-start': 123,
+ 'padding-bottom': 123,
+ 'padding-inline': 123,
+ 'padding-inline-end': 123,
+ 'padding-inline-start': 123,
+ 'padding-left': 123,
+ 'padding-right': 123,
+ 'padding-top': 123,
+ perspective: 123,
+ right: 123,
+ 'scroll-margin': 123,
+ 'scroll-margin-block': 123,
+ 'scroll-margin-block-start': 123,
+ 'scroll-margin-bottom': 123,
+ 'scroll-margin-inline': 123,
+ 'scroll-margin-inline-end': 123,
+ 'scroll-margin-inline-start': 123,
+ 'scroll-margin-inline-left': 123,
+ 'scroll-margin-inline-right': 123,
+ 'scroll-margin-inline-top': 123,
+ 'scroll-padding': 123,
+ 'scroll-padding-block': 123,
+ 'scroll-padding-block-end': 123,
+ 'scroll-padding-block-start': 123,
+ 'scroll-padding-bottom': 123,
+ 'scroll-padding-inline': 123,
+ 'scroll-padding-inline-end': 123,
+ 'scroll-padding-inline-start': 123,
+ 'scroll-padding-left': 123,
+ 'scroll-padding-right': 123,
+ 'scroll-padding-top': 123,
+ 'shape-margin': 123,
+ 'text-decoration-thickness': 123,
+ 'text-indent': 123,
+ 'text-underline-offset': 123,
+ top: 123,
+ 'transform-origin': 123,
+ translate: 123,
+ width: 123,
+ 'word-spacing': 123
+ };
+
+ // These are all CSS properties that have valid numeric values.
+ // Our appending logic must not be applied here
+ const untouched = {
+ '-webkit-line-clamp': 2,
+ animation: 2,
+ 'animation-iteration-count': 3,
+ 'border-image': 2,
+ 'border-image-slice': 2,
+ 'column-count': 2,
+ columns: 2,
+ flex: 1,
+ 'flex-grow': 1,
+ 'flex-shrink': 1,
+ 'font-size-adjust': 123,
+ 'font-weight': 12,
+ 'grid-column': 2,
+ 'grid-column-end': 2,
+ 'grid-column-start': 2,
+ 'grid-row': 2,
+ 'grid-row-end': 2,
+ 'grid-row-gap': 23,
+ 'grid-row-start': 2,
+ 'inital-letter': 2,
+ 'line-height': 2,
+ 'line-clamp': 2,
+ 'mask-border': 2,
+ 'mask-border-outset': 2,
+ 'mask-border-slice': 2,
+ 'mask-border-width': 2,
+ 'max-lines': 2,
+ 'max-zoom': 2,
+ 'min-zoom': 2,
+ opacity: 123,
+ order: 123,
+ orphans: 2,
+ scale: 23,
+ 'shape-image-threshold': 23,
+ 'tab-size': 23,
+ widows: 123,
+ 'z-index': 123,
+ zoom: 123
+ };
+
+ render(
+
,
+ scratch
+ );
+
+ let style = scratch.firstChild.style;
+
+ // Check properties that MUST not be changed
+ for (const key in unitless) {
+ // Check if css property is supported
+ if (
+ window.CSS &&
+ typeof window.CSS.supports === 'function' &&
+ window.CSS.supports(key, unitless[key])
+ ) {
+ expect(
+ String(style[key]).endsWith('px'),
+ `Should append px "${key}: ${unitless[key]}" === "${key}: ${style[key]}"`
+ ).to.equal(true);
+ }
+ }
+
+ // Check properties that MUST not be changed
+ for (const key in untouched) {
+ // Check if css property is supported
+ if (
+ window.CSS &&
+ typeof window.CSS.supports === 'function' &&
+ window.CSS.supports(key, untouched[key])
+ ) {
+ expect(
+ !String(style[key]).endsWith('px'),
+ `Should be left as is: "${key}: ${untouched[key]}" === "${key}: ${style[key]}"`
+ ).to.equal(true);
+ }
+ }
+ });
});
diff --git a/compat/test/browser/suspense-hydration.test.js b/compat/test/browser/suspense-hydration.test.js
index 3526b96918..72b9b23d2e 100644
--- a/compat/test/browser/suspense-hydration.test.js
+++ b/compat/test/browser/suspense-hydration.test.js
@@ -723,61 +723,17 @@ describe('suspense hydration', () => {
});
});
- // Currently not supported. Hydration doesn't set attributes... but should it
- // when coming back from suspense if props were updated?
- it.skip('should hydrate and update attributes with latest props', () => {
- const originalHtml = 'Count: 0
Lazy count: 0
';
- scratch.innerHTML = originalHtml;
- clearLog();
-
- /** @type {() => void} */
- let increment;
- const [Lazy, resolve] = createLazy();
- function App() {
- const [count, setCount] = useState(0);
- increment = () => setCount(c => c + 1);
-
- return (
-
- Count: {count}
-
-
- );
- }
-
- hydrate( , scratch);
- rerender(); // Flush rerender queue to mimic what preact will really do
- expect(scratch.innerHTML).to.equal(originalHtml);
- // Re: DOM OP below - Known issue with hydrating merged text nodes
- expect(getLog()).to.deep.equal(['Count: .appendChild(#text)']);
- clearLog();
-
- increment();
- rerender();
-
- expect(scratch.innerHTML).to.equal(
- '
Count: 1
Lazy count: 0
'
- );
- expect(getLog()).to.deep.equal([]);
- clearLog();
-
- return resolve(({ count }) => (
- Lazy count: {count}
- )).then(() => {
- rerender();
- expect(scratch.innerHTML).to.equal(
- 'Count: 1
Lazy count: 1
'
- );
- // Re: DOM OP below - Known issue with hydrating merged text nodes
- expect(getLog()).to.deep.equal(['Lazy count: .appendChild(#text)']);
- clearLog();
- });
- });
-
- // Currently not supported, but I wrote the test before I realized that so
- // leaving it here in case we do support it eventually
- it.skip('should properly hydrate suspense when resolves to a Fragment', () => {
- const originalHtml = ul([li(0), li(1), li(2), li(3), li(4), li(5)]);
+ it('should properly hydrate suspense when resolves to a Fragment', () => {
+ const originalHtml = ul([
+ li(0),
+ li(1),
+ '',
+ li(2),
+ li(3),
+ '',
+ li(4),
+ li(5)
+ ]);
const listeners = [
sinon.spy(),
@@ -809,8 +765,8 @@ describe('suspense hydration', () => {
scratch
);
rerender(); // Flush rerender queue to mimic what preact will really do
- expect(scratch.innerHTML).to.equal(originalHtml);
expect(getLog()).to.deep.equal([]);
+ expect(scratch.innerHTML).to.equal(originalHtml);
expect(listeners[5]).not.to.have.been.called;
clearLog();
@@ -839,4 +795,228 @@ describe('suspense hydration', () => {
expect(listeners[5]).to.have.been.calledTwice;
});
});
+
+ it('should properly hydrate suspense when resolves to a Fragment without children', () => {
+ const originalHtml = ul([
+ li(0),
+ li(1),
+ '',
+ '',
+ li(2),
+ li(3)
+ ]);
+
+ const listeners = [sinon.spy(), sinon.spy(), sinon.spy(), sinon.spy()];
+
+ scratch.innerHTML = originalHtml;
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+ hydrate(
+
+
+ 0
+ 1
+
+
+
+
+
+ 2
+ 3
+
+
,
+ scratch
+ );
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(getLog()).to.deep.equal([]);
+ expect(scratch.innerHTML).to.equal(originalHtml);
+ expect(listeners[3]).not.to.have.been.called;
+
+ clearLog();
+ scratch.querySelector('li:last-child').dispatchEvent(createEvent('click'));
+ expect(listeners[3]).to.have.been.calledOnce;
+
+ return resolve(() => null).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(originalHtml);
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ scratch
+ .querySelector('li:nth-child(2)')
+ .dispatchEvent(createEvent('click'));
+ expect(listeners[1]).to.have.been.calledOnce;
+
+ scratch
+ .querySelector('li:last-child')
+ .dispatchEvent(createEvent('click'));
+ expect(listeners[3]).to.have.been.calledTwice;
+ });
+ });
+
+ it('Should hydrate a fragment with multiple children correctly', () => {
+ scratch.innerHTML = '
Hello
World!
';
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+ hydrate(
+
+
+ ,
+ scratch
+ );
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal(
+ 'Hello
World!
'
+ );
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ return resolve(() => (
+ <>
+ Hello
+ World!
+ >
+ )).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'Hello
World!
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ });
+ });
+
+ it('Should hydrate a fragment with no children correctly', () => {
+ scratch.innerHTML = 'Hello world
';
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+ hydrate(
+ <>
+
+
+
+ Hello world
+ >,
+ scratch
+ );
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal(
+ 'Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ return resolve(() => null).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ });
+ });
+
+ it('Should hydrate a fragment with no children correctly deeply', () => {
+ scratch.innerHTML =
+ 'Hello world
';
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+ const [Lazy2, resolve2] = createLazy();
+ hydrate(
+ <>
+
+
+
+
+
+
+
+ Hello world
+ >,
+ scratch
+ );
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal(
+ 'Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ return resolve(p => p.children).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ return resolve2(() => null).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ });
+ });
+ });
+
+ it('Should hydrate a fragment with multiple children correctly deeply', () => {
+ scratch.innerHTML =
+ 'I am
Fragment Hello world
';
+ clearLog();
+
+ const [Lazy, resolve] = createLazy();
+ const [Lazy2, resolve2] = createLazy();
+ hydrate(
+ <>
+
+
+
+
+
+
+
+ Hello world
+ >,
+ scratch
+ );
+ rerender(); // Flush rerender queue to mimic what preact will really do
+ expect(scratch.innerHTML).to.equal(
+ 'I am
Fragment Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+ clearLog();
+
+ return resolve(p => p.children).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'I am
Fragment Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ return resolve2(() => (
+ <>
+ I am
+ Fragment
+ >
+ )).then(() => {
+ rerender();
+ expect(scratch.innerHTML).to.equal(
+ 'I am
Fragment Hello world
'
+ );
+ expect(getLog()).to.deep.equal([]);
+
+ clearLog();
+ });
+ });
+ });
});
diff --git a/compat/test/browser/suspense-list.test.js b/compat/test/browser/suspense-list.test.js
deleted file mode 100644
index 9e733c98cb..0000000000
--- a/compat/test/browser/suspense-list.test.js
+++ /dev/null
@@ -1,589 +0,0 @@
-import { setupRerender } from 'preact/test-utils';
-import React, {
- createElement,
- render,
- Component,
- Suspense,
- SuspenseList
-} from 'preact/compat';
-import { useState } from 'preact/hooks';
-import { setupScratch, teardown } from '../../../test/_util/helpers';
-
-const h = React.createElement;
-/* eslint-env browser, mocha */
-
-function getSuspendableComponent(text) {
- let resolve;
- let resolved = false;
- const promise = new Promise(_resolve => {
- resolve = () => {
- resolved = true;
- _resolve();
- return promise;
- };
- });
-
- class LifecycleSuspender extends Component {
- render() {
- if (!resolved) {
- throw promise;
- }
- return {text} ;
- }
- }
-
- LifecycleSuspender.resolve = () => {
- resolve();
- };
-
- return LifecycleSuspender;
-}
-
-describe('suspense-list', () => {
- /** @type {HTMLDivElement} */
- let scratch,
- rerender,
- unhandledEvents = [];
-
- function onUnhandledRejection(event) {
- unhandledEvents.push(event);
- }
-
- function getSuspenseList(revealOrder) {
- const A = getSuspendableComponent('A');
- const B = getSuspendableComponent('B');
- const C = getSuspendableComponent('C');
- render(
-
- Loading...}>
-
-
- Loading...}>
-
-
- Loading...}>
-
-
- ,
- scratch
- ); // Render initial state
-
- return [A.resolve, B.resolve, C.resolve];
- }
-
- function getNestedSuspenseList(outerRevealOrder, innerRevealOrder) {
- const A = getSuspendableComponent('A');
- const B = getSuspendableComponent('B');
- const C = getSuspendableComponent('C');
- const D = getSuspendableComponent('D');
-
- render(
-
- Loading...}>
-
-
-
- Loading...}>
-
-
- Loading...}>
-
-
-
- Loading...}>
-
-
- ,
- scratch
- );
- return [A.resolve, B.resolve, C.resolve, D.resolve];
- }
-
- beforeEach(() => {
- scratch = setupScratch();
- rerender = setupRerender();
- unhandledEvents = [];
-
- if ('onunhandledrejection' in window) {
- window.addEventListener('unhandledrejection', onUnhandledRejection);
- }
- });
-
- afterEach(() => {
- teardown(scratch);
-
- if ('onunhandledrejection' in window) {
- window.removeEventListener('unhandledrejection', onUnhandledRejection);
-
- if (unhandledEvents.length) {
- throw unhandledEvents[0].reason;
- }
- }
- });
-
- it('should work for single element', async () => {
- const Component = getSuspendableComponent('A');
- render(
-
- Loading...}>
-
-
- ,
- scratch
- ); // Render initial state
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(`Loading... `);
-
- await Component.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(`A `);
- });
-
- it('should let components appear backwards if no revealOrder is mentioned', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList();
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... B Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... B C `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should let components appear forwards if no revealOrder is mentioned', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList();
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should let components appear in forwards if revealOrder=forwards and first one resolves before others', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('forwards');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should make components appear together if revealOrder=forwards and others resolves before first', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('forwards');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should let components appear backwards if revealOrder=backwards and others resolves before first', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('backwards');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... C `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... B C `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should make components appear together if revealOrder=backwards and first one resolves others', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('backwards');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... C `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should make components appear together if revealOrder=together and first one resolves others', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('together');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should make components appear together if revealOrder=together and second one resolves before others', async () => {
- const [resolver1, resolver2, resolver3] = getSuspenseList('together');
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver2();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver1();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... `
- );
-
- await resolver3();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C `
- );
- });
-
- it('should not do anything to non suspense elements', async () => {
- const A = getSuspendableComponent('A');
- const B = getSuspendableComponent('B');
- render(
-
- Loading...}>
-
-
- foo
- Loading...}>
-
-
- bar
- ,
- scratch
- );
-
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... foo
Loading... bar `
- );
-
- await A.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A foo
Loading... bar `
- );
-
- await B.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A foo
B bar `
- );
- });
-
- it('should make sure nested SuspenseList works with forwards', async () => {
- const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
- 'forwards',
- 'forwards'
- );
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveB();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveA();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B Loading... Loading... `
- );
-
- await resolveC();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C Loading... `
- );
-
- await resolveD();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C D `
- );
- });
-
- it('should make sure nested SuspenseList works with backwards', async () => {
- const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
- 'forwards',
- 'backwards'
- );
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveA();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A Loading... Loading... Loading... `
- );
-
- await resolveC();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A Loading... C Loading... `
- );
-
- await resolveB();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C Loading... `
- );
-
- await resolveD();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C D `
- );
- });
-
- it('should make sure nested SuspenseList works with together', async () => {
- const [resolveA, resolveB, resolveC, resolveD] = getNestedSuspenseList(
- 'together',
- 'forwards'
- );
- rerender(); // Re-render with fallback cuz lazy threw
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveA();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveD();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveB();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `Loading... Loading... Loading... Loading... `
- );
-
- await resolveC();
- rerender();
- expect(scratch.innerHTML).to.eql(
- `A B C D `
- );
- });
-
- it('should work with forwards even when a child does not suspend', async () => {
- const Component = getSuspendableComponent('A');
-
- render(
-
- Loading...}>
-
-
- Loading...}>
-
-
- ,
- scratch
- ); // Render initial state
-
- rerender();
- expect(scratch.innerHTML).to.eql(`
Loading... `);
-
- await Component.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(`
A `);
- });
-
- it('should work with together even when a child does not suspend', async () => {
- const Component = getSuspendableComponent('A');
-
- render(
-
- Loading...}>
-
-
- Loading...}>
-
-
- ,
- scratch
- ); // Render initial state
-
- rerender();
- expect(scratch.innerHTML).to.eql(`
Loading... `);
-
- await Component.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(`
A `);
- });
-
- it('should not suspend resolved children if a new suspense comes in between', async () => {
- const ComponentA = getSuspendableComponent('A');
- const ComponentB = getSuspendableComponent('B');
-
- /** @type {(v) => void} */
- let showB;
- function Container() {
- const [showHidden, setShowHidden] = useState(false);
- showB = setShowHidden;
- return (
-
- Loading...}>
-
-
- {showHidden && (
- Loading...}>
-
-
- )}
- Loading...}>
-
-
-
- );
- }
- render( , scratch); // Render initial state
-
- rerender();
- expect(scratch.innerHTML).to.eql(`
Loading... `);
-
- await ComponentA.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(`
A `);
-
- showB(true);
- rerender();
- expect(scratch.innerHTML).to.eql(
- `
Loading... A `
- );
-
- await ComponentB.resolve();
- rerender();
- expect(scratch.innerHTML).to.eql(`
B A `);
- });
-});
diff --git a/compat/test/browser/suspense.test.js b/compat/test/browser/suspense.test.js
index d18dc8104f..6b6024855f 100644
--- a/compat/test/browser/suspense.test.js
+++ b/compat/test/browser/suspense.test.js
@@ -272,7 +272,7 @@ describe('suspense', () => {
});
it('lazy should forward refs', () => {
- const LazyComp = () => Hello from LazyComp
;
+ const LazyComp = props =>
;
let ref = {};
/** @type {() => Promise} */
@@ -298,7 +298,7 @@ describe('suspense', () => {
return resolve().then(() => {
rerender();
- expect(ref.current.constructor).to.equal(LazyComp);
+ expect(ref.current).to.equal(scratch.firstChild);
});
});
@@ -1675,8 +1675,17 @@ describe('suspense', () => {
// eslint-disable-next-line react/require-render-return
class Suspender extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { promise: new Promise(() => {}) };
+ if (typeof props.ref === 'function') {
+ props.ref(this);
+ } else if (props.ref) {
+ props.ref.current = this;
+ }
+ }
render() {
- throw new Promise(() => {});
+ throw this.state.promise;
}
}
diff --git a/compat/test/browser/textarea.test.js b/compat/test/browser/textarea.test.js
index d4cb83bb2c..d7306a0a40 100644
--- a/compat/test/browser/textarea.test.js
+++ b/compat/test/browser/textarea.test.js
@@ -33,17 +33,10 @@ describe('Textarea', () => {
hydrate( , scratch);
expect(scratch.firstElementChild.value).to.equal('foo');
- // IE11 always displays the value as node.innerHTML
- if (!/Trident/.test(window.navigator.userAgent)) {
- expect(scratch.innerHTML).to.be.equal('');
- }
+ expect(scratch.innerHTML).to.be.equal('');
});
it('should alias defaultValue to children', () => {
- // TODO: IE11 doesn't update `node.value` when
- // `node.defaultValue` is set.
- if (/Trident/.test(navigator.userAgent)) return;
-
render(, scratch);
expect(scratch.firstElementChild.value).to.equal('foo');
@@ -67,21 +60,13 @@ describe('Textarea', () => {
// missing from HTML because innerHTML doesn't serialize form field values.
// See demo: https://jsfiddle.net/4had2Lu8
// Related renderToString PR: preactjs/preact-render-to-string#161
- //
- // This is not true for IE11. It displays the value in
- // node.innerHTML regardless.
- if (!/Trident/.test(window.navigator.userAgent)) {
- expect(scratch.innerHTML).to.equal('');
- }
+ expect(scratch.innerHTML).to.equal('');
expect(scratch.firstElementChild.value).to.equal('hello');
act(() => {
set('');
});
- // Same as earlier: IE11 always displays the value as node.innerHTML
- if (!/Trident/.test(window.navigator.userAgent)) {
- expect(scratch.innerHTML).to.equal('');
- }
+ expect(scratch.innerHTML).to.equal('');
expect(scratch.firstElementChild.value).to.equal('');
});
});
diff --git a/compat/test/browser/unstable_batchedUpdates.test.js b/compat/test/browser/unstable_batchedUpdates.test.js
deleted file mode 100644
index 90d7f583a4..0000000000
--- a/compat/test/browser/unstable_batchedUpdates.test.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { unstable_batchedUpdates, flushSync } from 'preact/compat';
-
-describe('unstable_batchedUpdates', () => {
- it('should call the callback', () => {
- const spy = sinon.spy();
- unstable_batchedUpdates(spy);
- expect(spy).to.be.calledOnce;
- });
-
- it('should call callback with only one arg', () => {
- const spy = sinon.spy();
- // @ts-expect-error
- unstable_batchedUpdates(spy, 'foo', 'bar');
- expect(spy).to.be.calledWithExactly('foo');
- });
-});
-
-describe('flushSync', () => {
- it('should invoke the given callback', () => {
- const returnValue = {};
- const spy = sinon.spy(() => returnValue);
- const result = flushSync(spy);
- expect(spy).to.have.been.calledOnce;
- expect(result).to.equal(returnValue);
- });
-
- it('should invoke the given callback with the given argument', () => {
- const returnValue = {};
- const spy = sinon.spy(() => returnValue);
- const result = flushSync(spy, 'foo');
- expect(spy).to.be.calledWithExactly('foo');
- expect(result).to.equal(returnValue);
- });
-});
diff --git a/compat/test/ts/forward-ref.tsx b/compat/test/ts/forward-ref.tsx
index 25d0694079..98aa52b82e 100644
--- a/compat/test/ts/forward-ref.tsx
+++ b/compat/test/ts/forward-ref.tsx
@@ -1,9 +1,9 @@
import React from '../../src';
-const MyInput: React.ForwardFn<{ id: string }, { focus(): void }> = (
- props,
- ref
-) => {
+const MyInput: React.ForwardRefRenderFunction<
+ { focus(): void },
+ { id: string }
+> = (props, ref) => {
const inputRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
diff --git a/compat/test/ts/index.tsx b/compat/test/ts/index.tsx
index 2e322931d9..51dbd60adc 100644
--- a/compat/test/ts/index.tsx
+++ b/compat/test/ts/index.tsx
@@ -34,6 +34,8 @@ class SimpleComponentWithContextAsProvider extends React.Component {
}
}
+SimpleComponentWithContextAsProvider.defaultProps = { foo: 'default' };
+
React.render(
,
document.createElement('div')
diff --git a/compat/test/ts/suspense.tsx b/compat/test/ts/suspense.tsx
index c082f54663..3b6268f8a2 100644
--- a/compat/test/ts/suspense.tsx
+++ b/compat/test/ts/suspense.tsx
@@ -37,20 +37,6 @@ class ReactSuspensefulFunc extends React.Component {
}
}
-//SuspenseList using lazy components
-function ReactSuspenseListTester(_props: any) {
- return (
-
- }>
-
-
- }>
-
-
-
- );
-}
-
const Comp = () => Hello world
;
const importComponent = async () => {
diff --git a/config/node-13-exports.js b/config/node-13-exports.js
deleted file mode 100644
index 9528d2aefa..0000000000
--- a/config/node-13-exports.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const fs = require('fs');
-
-const subRepositories = [
- 'compat',
- 'debug',
- 'devtools',
- 'hooks',
- 'jsx-runtime',
- 'test-utils'
-];
-const snakeCaseToCamelCase = str =>
- str.replace(/([-_][a-z])/g, group => group.toUpperCase().replace('-', ''));
-
-const copyPreact = () => {
- // Copy .module.js --> .mjs for Node 13 compat.
- fs.writeFileSync(
- `${process.cwd()}/dist/preact.mjs`,
- fs.readFileSync(`${process.cwd()}/dist/preact.module.js`)
- );
-};
-
-const copy = name => {
- // Copy .module.js --> .mjs for Node 13 compat.
- const filename = name.includes('-') ? snakeCaseToCamelCase(name) : name;
- fs.writeFileSync(
- `${process.cwd()}/${name}/dist/${filename}.mjs`,
- fs.readFileSync(`${process.cwd()}/${name}/dist/${filename}.module.js`)
- );
-};
-
-copyPreact();
-subRepositories.forEach(copy);
diff --git a/debug/package.json b/debug/package.json
index 836b4b49f7..df80213a7a 100644
--- a/debug/package.json
+++ b/debug/package.json
@@ -5,7 +5,7 @@
"private": true,
"description": "Preact extensions for development",
"main": "dist/debug.js",
- "module": "dist/debug.module.js",
+ "module": "dist/debug.mjs",
"umd:main": "dist/debug.umd.js",
"source": "src/index.js",
"license": "MIT",
@@ -18,7 +18,7 @@
"exports": {
".": {
"types": "./src/index.d.ts",
- "browser": "./dist/debug.module.js",
+ "module": "./dist/debug.mjs",
"umd": "./dist/debug.umd.js",
"import": "./dist/debug.mjs",
"require": "./dist/debug.js"
diff --git a/debug/src/debug.js b/debug/src/debug.js
index 8da063130f..1d8eb7c0e6 100644
--- a/debug/src/debug.js
+++ b/debug/src/debug.js
@@ -11,7 +11,7 @@ import {
getCurrentVNode,
getDisplayName
} from './component-stack';
-import { assign, isNaN } from './util';
+import { isNaN } from './util';
const isWeakMapSupported = typeof WeakMap == 'function';
@@ -229,15 +229,12 @@ export function initDebug() {
}
}
- let values = vnode.props;
- if (vnode.type._forwarded) {
- values = assign({}, values);
- delete values.ref;
- }
+ /* eslint-disable-next-line */
+ const { ref: _ref, ...props } = vnode.props;
checkPropTypes(
vnode.type.propTypes,
- values,
+ props,
'prop',
getDisplayName(vnode),
() => getOwnerStack(vnode)
diff --git a/debug/src/util.js b/debug/src/util.js
index be4228b9b6..2dddcea736 100644
--- a/debug/src/util.js
+++ b/debug/src/util.js
@@ -1,14 +1,4 @@
-/**
- * Assign properties from `props` to `obj`
- * @template O, P The obj and props types
- * @param {O} obj The object to copy properties to
- * @param {P} props The object to copy properties from
- * @returns {O & P}
- */
-export function assign(obj, props) {
- for (let i in props) obj[i] = props[i];
- return /** @type {O & P} */ (obj);
-}
+export const assign = Object.assign;
export function isNaN(value) {
return value !== value;
diff --git a/devtools/package.json b/devtools/package.json
index c12ac730f0..353f2ad950 100644
--- a/devtools/package.json
+++ b/devtools/package.json
@@ -5,7 +5,7 @@
"private": true,
"description": "Preact bridge for Preact devtools",
"main": "dist/devtools.js",
- "module": "dist/devtools.module.js",
+ "module": "dist/devtools.mjs",
"umd:main": "dist/devtools.umd.js",
"source": "src/index.js",
"license": "MIT",
@@ -16,7 +16,7 @@
"exports": {
".": {
"types": "./src/index.d.ts",
- "browser": "./dist/devtools.module.js",
+ "module": "./dist/devtools.mjs",
"umd": "./dist/devtools.umd.js",
"import": "./dist/devtools.mjs",
"require": "./dist/devtools.js"
diff --git a/hooks/package.json b/hooks/package.json
index 787927573e..ea32c3fe2f 100644
--- a/hooks/package.json
+++ b/hooks/package.json
@@ -5,7 +5,7 @@
"private": true,
"description": "Hook addon for Preact",
"main": "dist/hooks.js",
- "module": "dist/hooks.module.js",
+ "module": "dist/hooks.mjs",
"umd:main": "dist/hooks.umd.js",
"source": "src/index.js",
"license": "MIT",
@@ -26,7 +26,7 @@
"exports": {
".": {
"types": "./src/index.d.ts",
- "browser": "./dist/hooks.module.js",
+ "module": "./dist/hooks.mjs",
"umd": "./dist/hooks.umd.js",
"import": "./dist/hooks.mjs",
"require": "./dist/hooks.js"
diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts
index d7f77dbb49..22147a2bd4 100644
--- a/hooks/src/index.d.ts
+++ b/hooks/src/index.d.ts
@@ -52,13 +52,6 @@ export function useReducer(
init: (arg: I) => S
): [S, Dispatch];
-/** @deprecated Use the `Ref` type instead. */
-type PropRef = MutableRef;
-
-interface MutableRef {
- current: T;
-}
-
/**
* `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.
@@ -68,9 +61,9 @@ interface MutableRef {
*
* @param initialValue the initial value to store in the ref object
*/
-export function useRef(initialValue: T): MutableRef;
-export function useRef(initialValue: T | null): RefObject;
-export function useRef(): MutableRef;
+export function useRef(initialValue: T): RefObject;
+export function useRef(initialValue: T | null): RefObject;
+export function useRef(initialValue: T | undefined): RefObject;
type EffectCallback = () => void | (() => void);
/**
diff --git a/hooks/src/index.js b/hooks/src/index.js
index b98b1988ae..eb52ae8e51 100644
--- a/hooks/src/index.js
+++ b/hooks/src/index.js
@@ -1,4 +1,7 @@
import { options as _options } from 'preact';
+import { SKIP_CHILDREN } from '../../src/constants';
+
+const ObjectIs = Object.is;
/** @type {number} */
let currentIndex;
@@ -24,6 +27,7 @@ let oldAfterDiff = options.diffed;
let oldCommit = options._commit;
let oldBeforeUnmount = options.unmount;
let oldRoot = options._root;
+let oldAfterRender = options._afterRender;
// We take the minimum timeout for requestAnimationFrame to ensure that
// the callback is invoked after the next frame. 35ms is based on a 30hz
@@ -58,10 +62,7 @@ options._render = vnode => {
hooks._pendingEffects = [];
currentComponent._renderCallbacks = [];
hooks._list.forEach(hookItem => {
- if (hookItem._nextValue) {
- hookItem._value = hookItem._nextValue;
- }
- hookItem._pendingArgs = hookItem._nextValue = undefined;
+ hookItem._pendingArgs = undefined;
});
} else {
hooks._pendingEffects.forEach(invokeCleanup);
@@ -184,19 +185,13 @@ export function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++, 2);
hookState._reducer = reducer;
if (!hookState._component) {
+ hookState._actions = [];
hookState._value = [
!init ? invokeOrReturn(undefined, initialState) : init(initialState),
action => {
- const currentValue = hookState._nextValue
- ? hookState._nextValue[0]
- : hookState._value[0];
- const nextValue = hookState._reducer(currentValue, action);
-
- if (currentValue !== nextValue) {
- hookState._nextValue = [nextValue, hookState._value[1]];
- hookState._component.setState({});
- }
+ hookState._actions.push(action);
+ hookState._component.setState({});
}
];
@@ -205,74 +200,55 @@ export function useReducer(reducer, initialState, init) {
if (!currentComponent._hasScuFromHooks) {
currentComponent._hasScuFromHooks = true;
let prevScu = currentComponent.shouldComponentUpdate;
- const prevCWU = currentComponent.componentWillUpdate;
-
- // If we're dealing with a forced update `shouldComponentUpdate` will
- // not be called. But we use that to update the hook values, so we
- // need to call it.
- currentComponent.componentWillUpdate = function (p, s, c) {
- if (this._force) {
- let tmp = prevScu;
- // Clear to avoid other sCU hooks from being called
- prevScu = undefined;
- updateHookState(p, s, c);
- prevScu = tmp;
- }
-
- if (prevCWU) prevCWU.call(this, p, s, c);
+
+ currentComponent.shouldComponentUpdate = function (p, s, c) {
+ return prevScu
+ ? prevScu.call(this, p, s, c) || hookState._actions.length
+ : hookState._actions.length;
};
+ }
+ }
- // This SCU has the purpose of bailing out after repeated updates
- // to stateful hooks.
- // we store the next value in _nextValue[0] and keep doing that for all
- // state setters, if we have next states and
- // all next states within a component end up being equal to their original state
- // we are safe to bail out for this specific component.
- /**
- *
- * @type {import('./internal').Component["shouldComponentUpdate"]}
- */
- // @ts-ignore - We don't use TS to downtranspile
- // eslint-disable-next-line no-inner-declarations
- function updateHookState(p, s, c) {
- if (!hookState._component.__hooks) return true;
-
- /** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
- const isStateHook = x => !!x._component;
- const stateHooks =
- hookState._component.__hooks._list.filter(isStateHook);
-
- const allHooksEmpty = stateHooks.every(x => !x._nextValue);
- // When we have no updated hooks in the component we invoke the previous SCU or
- // traverse the VDOM tree further.
- if (allHooksEmpty) {
- return prevScu ? prevScu.call(this, p, s, c) : true;
- }
-
- // We check whether we have components with a nextValue set that
- // have values that aren't equal to one another this pushes
- // us to update further down the tree
- let shouldUpdate = hookState._component.props !== p;
- stateHooks.forEach(hookItem => {
- if (hookItem._nextValue) {
- const currentValue = hookItem._value[0];
- hookItem._value = hookItem._nextValue;
- hookItem._nextValue = undefined;
- if (currentValue !== hookItem._value[0]) shouldUpdate = true;
- }
- });
+ if (hookState._actions.length) {
+ const initialValue = hookState._value[0];
+ hookState._actions.some(action => {
+ hookState._value[0] = hookState._reducer(hookState._value[0], action);
+ });
- return prevScu
- ? prevScu.call(this, p, s, c) || shouldUpdate
- : shouldUpdate;
- }
+ hookState._didUpdate = !ObjectIs(initialValue, hookState._value[0]);
+ hookState._value = [hookState._value[0], hookState._value[1]];
+ hookState._didExecute = true;
+ hookState._actions = [];
+ }
- currentComponent.shouldComponentUpdate = updateHookState;
+ return hookState._value;
+}
+
+options._afterRender = (newVNode, oldVNode) => {
+ if (newVNode._component && newVNode._component.__hooks) {
+ const hooks = newVNode._component.__hooks._list;
+ const stateHooksThatExecuted = hooks.filter(
+ /** @type {(x: import('./internal').HookState) => x is import('./internal').ReducerHookState} */
+ // @ts-expect-error
+ x => x._component && x._didExecute
+ );
+
+ if (
+ stateHooksThatExecuted.length &&
+ !stateHooksThatExecuted.some(x => x._didUpdate) &&
+ oldVNode.props === newVNode.props
+ ) {
+ newVNode._component.__hooks._pendingEffects = [];
+ newVNode._flags |= SKIP_CHILDREN;
}
+
+ stateHooksThatExecuted.some(hook => {
+ hook._didExecute = hook._didUpdate = false;
+ });
}
- return hookState._nextValue || hookState._value;
-}
+ if (oldAfterRender) oldAfterRender(newVNode, oldVNode);
+};
/**
* @param {import('./internal').Effect} callback
@@ -540,7 +516,7 @@ function argsChanged(oldArgs, newArgs) {
return (
!oldArgs ||
oldArgs.length !== newArgs.length ||
- newArgs.some((arg, index) => arg !== oldArgs[index])
+ newArgs.some((arg, index) => !ObjectIs(arg, oldArgs[index]))
);
}
diff --git a/hooks/src/internal.d.ts b/hooks/src/internal.d.ts
index 76cd97812b..b5b6d66c99 100644
--- a/hooks/src/internal.d.ts
+++ b/hooks/src/internal.d.ts
@@ -4,7 +4,7 @@ import {
VNode as PreactVNode,
PreactContext,
HookType,
- ErrorInfo,
+ ErrorInfo
} from '../../src/internal';
import { Reducer, StateUpdater } from '.';
@@ -32,7 +32,8 @@ export interface ComponentHooks {
_pendingEffects: EffectHookState[];
}
-export interface Component extends Omit, '_renderCallbacks'> {
+export interface Component
+ extends Omit, '_renderCallbacks'> {
__hooks?: ComponentHooks;
// Extend to include HookStates
_renderCallbacks?: Array void)>;
@@ -54,8 +55,6 @@ export type HookState =
interface BaseHookState {
_value?: unknown;
- _nextValue?: unknown;
- _pendingValue?: unknown;
_args?: unknown;
_pendingArgs?: unknown;
_component?: unknown;
@@ -74,7 +73,6 @@ export interface EffectHookState extends BaseHookState {
export interface MemoHookState extends BaseHookState {
_value?: T;
- _pendingValue?: T;
_args?: unknown[];
_pendingArgs?: unknown[];
_factory?: () => T;
@@ -82,10 +80,12 @@ export interface MemoHookState extends BaseHookState {
export interface ReducerHookState
extends BaseHookState {
- _nextValue?: [S, StateUpdater];
_value?: [S, StateUpdater];
+ _actions?: any[];
_component?: Component;
_reducer?: Reducer;
+ _didExecute?: boolean;
+ _didUpdate?: boolean;
}
export interface ContextHookState extends BaseHookState {
diff --git a/hooks/test/browser/useContext.test.js b/hooks/test/browser/useContext.test.js
index 629ab06714..36baaad45c 100644
--- a/hooks/test/browser/useContext.test.js
+++ b/hooks/test/browser/useContext.test.js
@@ -185,6 +185,7 @@ describe('useContext', () => {
let provider, subSpy;
function Comp() {
+ provider = this._vnode._parent._component;
const value = useContext(Context);
values.push(value);
return null;
@@ -193,7 +194,7 @@ describe('useContext', () => {
render( , scratch);
render(
- (provider = p)} value={42}>
+
,
scratch
@@ -217,6 +218,7 @@ describe('useContext', () => {
let provider, subSpy;
function Comp() {
+ provider = this._vnode._parent._component;
const value = useContext(Context);
values.push(value);
return null;
@@ -225,7 +227,7 @@ describe('useContext', () => {
render( , scratch);
render(
- (provider = p)} value={42}>
+
,
scratch
diff --git a/hooks/test/browser/useEffect.test.js b/hooks/test/browser/useEffect.test.js
index feb554192d..550fc41741 100644
--- a/hooks/test/browser/useEffect.test.js
+++ b/hooks/test/browser/useEffect.test.js
@@ -636,4 +636,26 @@ describe('useEffect', () => {
expect(calls.length).to.equal(1);
expect(calls).to.deep.equal(['doing effecthi']);
});
+
+ it('should not rerun when receiving NaN on subsequent renders', () => {
+ const calls = [];
+ const Component = ({ value }) => {
+ const [count, setCount] = useState(0);
+ useEffect(() => {
+ calls.push('doing effect' + count);
+ setCount(count + 1);
+ return () => {
+ calls.push('cleaning up' + count);
+ };
+ }, [value]);
+ return {count}
;
+ };
+ const App = () => ;
+
+ act(() => {
+ render( , scratch);
+ });
+ expect(calls.length).to.equal(1);
+ expect(calls).to.deep.equal(['doing effect0']);
+ });
});
diff --git a/hooks/test/browser/useState.test.js b/hooks/test/browser/useState.test.js
index e58552c472..29e722c4c2 100644
--- a/hooks/test/browser/useState.test.js
+++ b/hooks/test/browser/useState.test.js
@@ -1,7 +1,14 @@
import { setupRerender, act } from 'preact/test-utils';
import { createElement, render, createContext, Component } from 'preact';
-import { vi } from 'vitest';
-import { useState, useContext, useEffect } from 'preact/hooks';
+import { afterAll, beforeAll, expect, vi } from 'vitest';
+import {
+ useState,
+ useContext,
+ useEffect,
+ useLayoutEffect,
+ useReducer,
+ useRef
+} from 'preact/hooks';
import { setupScratch, teardown } from '../../../test/_util/helpers';
/** @jsx createElement */
@@ -70,12 +77,12 @@ describe('useState', () => {
doSetState(0);
rerender();
expect(lastState).to.equal(0);
- expect(Comp).toHaveBeenCalledOnce();
+ expect(Comp).toHaveBeenCalledTimes(2);
doSetState(() => 0);
rerender();
expect(lastState).to.equal(0);
- expect(Comp).toHaveBeenCalledOnce();
+ expect(Comp).toHaveBeenCalledTimes(3);
});
it('rerenders when setting the state', () => {
@@ -345,7 +352,7 @@ describe('useState', () => {
render( , scratch);
});
- expect(renderSpy).to.be.calledTwice;
+ expect(renderSpy).to.be.calledThrice;
});
// see preactjs/preact#3731
@@ -373,6 +380,79 @@ describe('useState', () => {
expect(scratch.innerHTML).to.equal('hello world!!!
');
});
+ it('should exhaust renders when NaN state is set as a result of a props update', () => {
+ const calls = [];
+ const App = ({ i }) => {
+ calls.push('rendering' + i);
+ const [greeting, setGreeting] = useState(0);
+
+ if (i === 2) {
+ setGreeting(NaN);
+ }
+
+ return {greeting}
;
+ };
+
+ act(() => {
+ render( , scratch);
+ });
+ expect(calls.length).to.equal(1);
+ expect(calls).to.deep.equal(['rendering1']);
+
+ act(() => {
+ render( , scratch);
+ });
+ expect(calls.length).to.equal(27);
+ expect(calls.slice(1).every(c => c === 'rendering2')).to.equal(true);
+ });
+
+ it('should bail correctly when setting NaN twice', () => {
+ const calls = [];
+ let set;
+ const Greeting = ({ greeting }) => {
+ calls.push('rendering ' + greeting);
+ return {greeting}
;
+ };
+ const App = () => {
+ const [greeting, setGreeting] = useState(0);
+ set = setGreeting;
+
+ return ;
+ };
+
+ act(() => {
+ render( , scratch);
+ });
+ expect(calls.length).to.equal(1);
+ expect(calls).to.deep.equal(['rendering 0']);
+
+ act(() => {
+ set(1);
+ });
+ expect(calls.length).to.equal(2);
+ expect(calls).to.deep.equal(['rendering 0', 'rendering 1']);
+
+ act(() => {
+ set(NaN);
+ });
+ expect(calls.length).to.equal(3);
+ expect(calls).to.deep.equal([
+ 'rendering 0',
+ 'rendering 1',
+ 'rendering NaN'
+ ]);
+
+ act(() => {
+ set(NaN);
+ });
+ expect(calls.length).to.equal(3);
+ expect(calls).to.deep.equal([
+ 'rendering 0',
+ 'rendering 1',
+ 'rendering NaN'
+ ]);
+ });
+
describe('Global sCU', () => {
let prevScu;
beforeAll(() => {
@@ -414,4 +494,94 @@ describe('useState', () => {
expect(renders).to.equal(2);
});
});
+
+ it('Should capture the closure in the reducer', () => {
+ function createContext2() {
+ const context = createContext();
+
+ const ProviderOrig = context.Provider;
+ context.Provider = ({ value, children }) => {
+ const valueRef = useRef(value);
+ const contextValue = useRef();
+
+ if (!contextValue.current) {
+ contextValue.current = {
+ value: valueRef,
+ listener: null
+ };
+ }
+
+ useLayoutEffect(() => {
+ valueRef.current = value;
+ if (contextValue.current.listener) {
+ contextValue.current.listener([value]);
+ }
+ }, [value]);
+ return (
+ {children}
+ );
+ };
+
+ return context;
+ }
+
+ function useContextSelector(context) {
+ const contextValue = useContext(context);
+ const {
+ value: { current: value }
+ } = contextValue;
+ const [state, dispatch] = useReducer(
+ () => {
+ return {
+ value
+ };
+ },
+ {
+ value
+ }
+ );
+ useLayoutEffect(() => {
+ contextValue.listener = dispatch;
+ }, []);
+ return state.value;
+ }
+
+ const context = createContext2();
+ let set;
+
+ function Child() {
+ const [count, setState] = useContextSelector(context);
+ const [c, setC] = useState(0);
+ set = () => {
+ setC(s => s + 1);
+ setState(s => s + 1);
+ };
+ return (
+
+
Context count: {count}
+
Local count: {c}
+
+ );
+ }
+
+ // Render this
+ function App() {
+ const [state, setState] = useState(0);
+ return (
+
+
+
+ );
+ }
+
+ act(() => {
+ render( , scratch);
+ });
+ expect(scratch.textContent).to.equal('Context count: 0Local count: 0');
+
+ act(() => {
+ set();
+ });
+ expect(scratch.textContent).to.equal('Context count: 1Local count: 1');
+ });
});
diff --git a/jsx-runtime/package.json b/jsx-runtime/package.json
index 1014de1c82..6a0cf9cd1c 100644
--- a/jsx-runtime/package.json
+++ b/jsx-runtime/package.json
@@ -5,7 +5,7 @@
"private": true,
"description": "Preact JSX runtime",
"main": "dist/jsxRuntime.js",
- "module": "dist/jsxRuntime.module.js",
+ "module": "dist/jsxRuntime.mjs",
"umd:main": "dist/jsxRuntime.umd.js",
"source": "src/index.js",
"types": "src/index.d.ts",
@@ -19,7 +19,7 @@
"exports": {
".": {
"types": "./src/index.d.ts",
- "browser": "./dist/jsxRuntime.module.js",
+ "module": "./dist/jsxRuntime.mjs",
"umd": "./dist/jsxRuntime.umd.js",
"import": "./dist/jsxRuntime.mjs",
"require": "./dist/jsxRuntime.js"
diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js
index a6a21b8b03..8d7cbaa7f6 100644
--- a/jsx-runtime/src/index.js
+++ b/jsx-runtime/src/index.js
@@ -1,6 +1,5 @@
import { options, Fragment } from 'preact';
import { encodeEntities } from './utils';
-import { IS_NON_DIMENSIONAL } from '../../src/constants';
let vnodeId = 0;
@@ -19,9 +18,9 @@ const isArray = Array.isArray;
/**
* JSX.Element factory used by Babel's {runtime:"automatic"} JSX transform
- * @param {VNode['type']} type
- * @param {VNode['props']} props
- * @param {VNode['key']} [key]
+ * @param {import('../../src/internal').VNode['type']} type
+ * @param {import('preact').VNode['props']} props
+ * @param {import('preact').VNode['key']} [key]
* @param {unknown} [isStaticChildren]
* @param {unknown} [__source]
* @param {unknown} [__self]
@@ -35,7 +34,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
ref,
i;
- if ('ref' in normalizedProps) {
+ if ('ref' in normalizedProps && typeof type != 'function') {
normalizedProps = {};
for (i in props) {
if (i == 'ref') {
@@ -46,7 +45,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
}
}
- /** @type {VNode & { __source: any; __self: any }} */
+ /** @type {import('../../src/internal').VNode & { __source: any; __self: any }} */
const vnode = {
type,
props: normalizedProps,
@@ -65,15 +64,6 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
__self
};
- // If a Component VNode, check for and apply defaultProps.
- // Note: `type` is often a String, and can be `undefined` in development.
- if (typeof type === 'function' && (ref = type.defaultProps)) {
- for (i in ref)
- if (normalizedProps[i] === undefined) {
- normalizedProps[i] = ref[i];
- }
- }
-
if (options.vnode) options.vnode(vnode);
return vnode;
}
@@ -82,12 +72,13 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
* Create a template vnode. This function is not expected to be
* used directly, but rather through a precompile JSX transform
* @param {string[]} templates
- * @param {Array} exprs
- * @returns {VNode}
+ * @param {Array} exprs
+ * @returns {import('preact').VNode}
*/
function jsxTemplate(templates, ...exprs) {
const vnode = createVNode(Fragment, { tpl: templates, exprs });
// Bypass render to string top level Fragment optimization
+ // @ts-ignore
vnode.key = vnode._vnode;
return vnode;
}
@@ -121,16 +112,7 @@ function jsxAttr(name, value) {
: JS_TO_CSS[prop] ||
(JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
- let suffix = ';';
- if (
- typeof val === 'number' &&
- // Exclude custom-attributes
- !name.startsWith('--') &&
- !IS_NON_DIMENSIONAL.test(name)
- ) {
- suffix = 'px;';
- }
- str = str + name + ':' + val + suffix;
+ str = str + name + ':' + val + ';';
}
}
return name + '="' + str + '"';
@@ -153,7 +135,7 @@ function jsxAttr(name, value) {
* is not expected to be used directly, but rather through a
* precompile JSX transform
* @param {*} value
- * @returns {string | null | VNode | Array}
+ * @returns {string | null | import('preact').VNode | Array}
*/
function jsxEscape(value) {
if (
diff --git a/jsx-runtime/test/browser/jsx-runtime.test.js b/jsx-runtime/test/browser/jsx-runtime.test.js
index a5ba0daaf0..d06f8da21d 100644
--- a/jsx-runtime/test/browser/jsx-runtime.test.js
+++ b/jsx-runtime/test/browser/jsx-runtime.test.js
@@ -51,46 +51,6 @@ describe('Babel jsx/jsxDEV', () => {
expect(vnode.key).to.equal('foo');
});
- it('should apply defaultProps', () => {
- class Foo extends Component {
- render() {
- return
;
- }
- }
-
- Foo.defaultProps = {
- foo: 'bar'
- };
-
- const vnode = jsx(Foo, {}, null);
- expect(vnode.props).to.deep.equal({
- foo: 'bar'
- });
- });
-
- it('should respect defaultProps when props are null', () => {
- const Component = ({ children }) => children;
- Component.defaultProps = { foo: 'bar' };
- expect(jsx(Component, { foo: null }).props).to.deep.equal({ foo: null });
- });
-
- it('should keep props over defaultProps', () => {
- class Foo extends Component {
- render() {
- return
;
- }
- }
-
- Foo.defaultProps = {
- foo: 'bar'
- };
-
- const vnode = jsx(Foo, { foo: 'baz' }, null);
- expect(vnode.props).to.deep.equal({
- foo: 'baz'
- });
- });
-
it('should set __source and __self', () => {
const vnode = jsx('div', { class: 'foo' }, 'key', false, 'source', 'self');
expect(vnode.__source).to.equal('source');
@@ -173,7 +133,9 @@ describe('precompiled JSX', () => {
});
it('should serialize style object', () => {
- expect(jsxAttr('style', { padding: 3 })).to.equal('style="padding:3px;"');
+ expect(jsxAttr('style', { padding: '3px' })).to.equal(
+ 'style="padding:3px;"'
+ );
});
});
diff --git a/mangle.json b/mangle.json
index 2d15afef53..bfe7a0b10b 100644
--- a/mangle.json
+++ b/mangle.json
@@ -26,22 +26,25 @@
"cname": 6,
"props": {
"$_hasScuFromHooks": "__f",
- "$_listeners": "l",
+ "$_listeners": "__l",
"$_cleanup": "__c",
"$__hooks": "__H",
"$_hydrationMismatch": "__m",
"$_list": "__",
"$_pendingEffects": "__h",
"$_value": "__",
- "$_nextValue": "__N",
+ "$_didExecute": "__N",
+ "$_didUpdate": "__U",
"$_original": "__v",
"$_args": "__H",
"$_factory": "__h",
"$_depth": "__b",
"$_dirty": "__d",
+ "$_afterRender": "__d",
"$_mask": "__m",
"$_detachOnNextRender": "__b",
"$_force": "__e",
+ "$_excess": "__z",
"$_nextState": "__s",
"$_renderCallbacks": "__h",
"$_stateCallbacks": "_sb",
diff --git a/package.json b/package.json
index 7f7f39d40d..12a8685f25 100644
--- a/package.json
+++ b/package.json
@@ -3,9 +3,9 @@
"amdName": "preact",
"version": "10.26.8",
"private": false,
- "description": "Fast 3kb React-compatible Virtual DOM library.",
+ "description": "Fast 4kb React-compatible Virtual DOM library.",
"main": "dist/preact.js",
- "module": "dist/preact.module.js",
+ "module": "dist/preact.mjs",
"umd:main": "dist/preact.umd.js",
"unpkg": "dist/preact.min.js",
"source": "src/index.js",
@@ -20,56 +20,56 @@
"types": "./src/index-5.d.ts"
},
"types": "./src/index.d.ts",
- "browser": "./dist/preact.module.js",
+ "module": "./dist/preact.mjs",
"umd": "./dist/preact.umd.js",
"import": "./dist/preact.mjs",
"require": "./dist/preact.js"
},
"./compat": {
"types": "./compat/src/index.d.ts",
- "browser": "./compat/dist/compat.module.js",
+ "module": "./compat/dist/compat.mjs",
"umd": "./compat/dist/compat.umd.js",
"import": "./compat/dist/compat.mjs",
"require": "./compat/dist/compat.js"
},
"./debug": {
"types": "./debug/src/index.d.ts",
- "browser": "./debug/dist/debug.module.js",
+ "module": "./debug/dist/debug.mjs",
"umd": "./debug/dist/debug.umd.js",
"import": "./debug/dist/debug.mjs",
"require": "./debug/dist/debug.js"
},
"./devtools": {
"types": "./devtools/src/index.d.ts",
- "browser": "./devtools/dist/devtools.module.js",
+ "module": "./devtools/dist/devtools.mjs",
"umd": "./devtools/dist/devtools.umd.js",
"import": "./devtools/dist/devtools.mjs",
"require": "./devtools/dist/devtools.js"
},
"./hooks": {
"types": "./hooks/src/index.d.ts",
- "browser": "./hooks/dist/hooks.module.js",
+ "module": "./hooks/dist/hooks.mjs",
"umd": "./hooks/dist/hooks.umd.js",
"import": "./hooks/dist/hooks.mjs",
"require": "./hooks/dist/hooks.js"
},
"./test-utils": {
"types": "./test-utils/src/index.d.ts",
- "browser": "./test-utils/dist/testUtils.module.js",
+ "module": "./test-utils/dist/testUtils.mjs",
"umd": "./test-utils/dist/testUtils.umd.js",
"import": "./test-utils/dist/testUtils.mjs",
"require": "./test-utils/dist/testUtils.js"
},
"./jsx-runtime": {
"types": "./jsx-runtime/src/index.d.ts",
- "browser": "./jsx-runtime/dist/jsxRuntime.module.js",
+ "module": "./jsx-runtime/dist/jsxRuntime.mjs",
"umd": "./jsx-runtime/dist/jsxRuntime.umd.js",
"import": "./jsx-runtime/dist/jsxRuntime.mjs",
"require": "./jsx-runtime/dist/jsxRuntime.js"
},
"./jsx-dev-runtime": {
"types": "./jsx-runtime/src/index.d.ts",
- "browser": "./jsx-runtime/dist/jsxRuntime.module.js",
+ "module": "./jsx-runtime/dist/jsxRuntime.mjs",
"umd": "./jsx-runtime/dist/jsxRuntime.umd.js",
"import": "./jsx-runtime/dist/jsxRuntime.mjs",
"require": "./jsx-runtime/dist/jsxRuntime.js"
@@ -116,14 +116,13 @@
"prepare": "husky && run-s build",
"build": "npm-run-all --parallel 'build:*'",
"build:core": "microbundle build --raw --no-generateTypes -f cjs,esm,umd",
- "build:core-min": "microbundle build --raw --no-generateTypes -f cjs,esm,umd,iife src/cjs.js -o dist/preact.min.js",
"build:debug": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd debug",
"build:devtools": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd devtools",
"build:hooks": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd hooks",
"build:test-utils": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd test-utils",
"build:compat": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd compat --globals 'preact/hooks=preactHooks'",
"build:jsx": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd jsx-runtime",
- "postbuild": "node ./config/node-13-exports.js && node ./config/compat-entries.js",
+ "postbuild": "node ./config/compat-entries.js",
"dev": "microbundle watch --raw --no-generateTypes --format cjs",
"dev:hooks": "microbundle watch --raw --no-generateTypes --format cjs --cwd hooks",
"dev:compat": "microbundle watch --raw --no-generateTypes --format cjs --cwd compat --globals 'preact/hooks=preactHooks'",
diff --git a/src/cjs.js b/src/cjs.js
deleted file mode 100644
index b4721b1d44..0000000000
--- a/src/cjs.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import * as preact from './index.js';
-if (typeof module < 'u') module.exports = preact;
-else self.preact = preact;
diff --git a/src/clone-element.js b/src/clone-element.js
index 671eb4e63f..b2f6e118cf 100644
--- a/src/clone-element.js
+++ b/src/clone-element.js
@@ -1,6 +1,6 @@
import { assign, slice } from './util';
import { createVNode } from './create-element';
-import { NULL, UNDEFINED } from './constants';
+import { NULL } from './constants';
/**
* Clones the given VNode, optionally adding attributes/props and replacing its
@@ -17,20 +17,10 @@ export function cloneElement(vnode, props, children) {
ref,
i;
- let defaultProps;
-
- if (vnode.type && vnode.type.defaultProps) {
- defaultProps = vnode.type.defaultProps;
- }
-
for (i in props) {
if (i == 'key') key = props[i];
- else if (i == 'ref') ref = props[i];
- else if (props[i] === UNDEFINED && defaultProps != UNDEFINED) {
- normalizedProps[i] = defaultProps[i];
- } else {
- normalizedProps[i] = props[i];
- }
+ else if (i == 'ref' && typeof vnode.type != 'function') ref = props[i];
+ else normalizedProps[i] = props[i];
}
if (arguments.length > 2) {
diff --git a/src/component.js b/src/component.js
index b233d8c7c0..9b1327629b 100644
--- a/src/component.js
+++ b/src/component.js
@@ -159,11 +159,11 @@ function renderComponent(component) {
*/
function updateParentDomPointers(vnode) {
if ((vnode = vnode._parent) != NULL && vnode._component != NULL) {
- vnode._dom = vnode._component.base = NULL;
+ vnode._dom = NULL;
for (let i = 0; i < vnode._children.length; i++) {
let child = vnode._children[i];
if (child != NULL && child._dom != NULL) {
- vnode._dom = vnode._component.base = child._dom;
+ vnode._dom = child._dom;
break;
}
}
@@ -189,11 +189,6 @@ let rerenderQueue = [];
let prevDebounce;
-const defer =
- typeof Promise == 'function'
- ? Promise.prototype.then.bind(Promise.resolve())
- : setTimeout;
-
/**
* Enqueue a rerender of a component
* @param {import('./internal').Component} c The component to rerender
@@ -207,7 +202,7 @@ export function enqueueRender(c) {
prevDebounce != options.debounceRendering
) {
prevDebounce = options.debounceRendering;
- (prevDebounce || defer)(process);
+ (prevDebounce || queueMicrotask)(process);
}
}
diff --git a/src/constants.js b/src/constants.js
index c60df07b93..57d56d98c6 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -6,6 +6,8 @@ export const MODE_SUSPENDED = 1 << 7;
export const INSERT_VNODE = 1 << 2;
/** Indicates a VNode has been matched with another VNode in the diff */
export const MATCHED = 1 << 1;
+/** Indicates that children should not be diffed */
+export const SKIP_CHILDREN = 1 << 3;
/** Reset all mode flags */
export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED);
@@ -18,5 +20,3 @@ export const NULL = null;
export const UNDEFINED = undefined;
export const EMPTY_OBJ = /** @type {any} */ ({});
export const EMPTY_ARR = [];
-export const IS_NON_DIMENSIONAL =
- /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;
diff --git a/src/create-element.js b/src/create-element.js
index d4ea2dd935..0c0990ff60 100644
--- a/src/create-element.js
+++ b/src/create-element.js
@@ -20,7 +20,7 @@ export function createElement(type, props, children) {
i;
for (i in props) {
if (i == 'key') key = props[i];
- else if (i == 'ref') ref = props[i];
+ else if (i == 'ref' && typeof type != 'function') ref = props[i];
else normalizedProps[i] = props[i];
}
@@ -29,16 +29,6 @@ export function createElement(type, props, children) {
arguments.length > 3 ? slice.call(arguments, 2) : children;
}
- // If a Component VNode, check for and apply defaultProps
- // Note: type may be undefined in development, must never error here.
- if (typeof type == 'function' && type.defaultProps != NULL) {
- for (i in type.defaultProps) {
- if (normalizedProps[i] === UNDEFINED) {
- normalizedProps[i] = type.defaultProps[i];
- }
- }
- }
-
return createVNode(type, normalizedProps, key, ref, NULL);
}
diff --git a/src/diff/children.js b/src/diff/children.js
index 25ebcd3660..7f401a0373 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -358,7 +358,7 @@ function insert(parentVNode, oldDom, parentDom) {
return oldDom;
} else if (parentVNode._dom != oldDom) {
- if (oldDom && parentVNode.type && !parentDom.contains(oldDom)) {
+ if (oldDom && parentVNode.type && !oldDom.parentNode) {
oldDom = getDomSibling(parentVNode);
}
parentDom.insertBefore(parentVNode._dom, oldDom || NULL);
diff --git a/src/diff/index.js b/src/diff/index.js
index 5603586ae6..d9a62cbae5 100644
--- a/src/diff/index.js
+++ b/src/diff/index.js
@@ -5,6 +5,7 @@ import {
MODE_SUSPENDED,
NULL,
RESET_MODE,
+ SKIP_CHILDREN,
SVG_NAMESPACE,
UNDEFINED,
XHTML_NAMESPACE
@@ -69,8 +70,11 @@ export function diff(
// If the previous diff bailed out, resume creating/hydrating.
if (oldVNode._flags & MODE_SUSPENDED) {
isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
- oldDom = newVNode._dom = oldVNode._dom;
- excessDomChildren = [oldDom];
+ if (oldVNode._component._excess) {
+ excessDomChildren = oldVNode._component._excess;
+ oldDom = newVNode._dom = oldVNode._dom = excessDomChildren[0];
+ oldVNode._component._excess = null;
+ }
}
if ((tmp = options._diff)) tmp(newVNode);
@@ -220,6 +224,7 @@ export function diff(
c._force = false;
let renderHook = options._render,
+ afterRender = options._afterRender,
count = 0;
if (isClassComponent) {
c.state = c._nextState;
@@ -228,6 +233,7 @@ export function diff(
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
+ if (afterRender) afterRender(newVNode, oldVNode);
for (let i = 0; i < c._stateCallbacks.length; i++) {
c._renderCallbacks.push(c._stateCallbacks[i]);
@@ -239,6 +245,18 @@ export function diff(
if (renderHook) renderHook(newVNode);
tmp = c.render(c.props, c.state, c.context);
+ if (afterRender) afterRender(newVNode, oldVNode);
+
+ if (newVNode._flags & SKIP_CHILDREN) {
+ c._dirty = false;
+ c._renderCallbacks = [];
+ newVNode._dom = oldVNode._dom;
+ newVNode._children = oldVNode._children;
+ newVNode._children.some(vnode => {
+ if (vnode) vnode._parent = newVNode;
+ });
+ break outer;
+ }
// Handle setState called in render, see #2553
c.state = c._nextState;
@@ -249,7 +267,7 @@ export function diff(
c.state = c._nextState;
if (c.getChildContext != NULL) {
- globalContext = assign(assign({}, globalContext), c.getChildContext());
+ globalContext = assign({}, globalContext, c.getChildContext());
}
if (isClassComponent && !isNew && c.getSnapshotBeforeUpdate != NULL) {
@@ -278,8 +296,6 @@ export function diff(
refQueue
);
- c.base = newVNode._dom;
-
// We successfully rendered this VNode, unset any stored hydration/bailout state:
newVNode._flags &= RESET_MODE;
@@ -295,15 +311,54 @@ export function diff(
// if hydrating or creating initial tree, bailout preserves DOM:
if (isHydrating || excessDomChildren != NULL) {
if (e.then) {
+ let commentMarkersToFind = 0,
+ done = false;
+
newVNode._flags |= isHydrating
? MODE_HYDRATE | MODE_SUSPENDED
: MODE_SUSPENDED;
- while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
- oldDom = oldDom.nextSibling;
+ newVNode._component._excess = [];
+ for (let i = 0; i < excessDomChildren.length; i++) {
+ let child = excessDomChildren[i];
+ if (child == NULL || done) continue;
+
+ // When we encounter a boundary with $s we are opening
+ // a boundary, this implies that we need to bump
+ // the amount of markers we need to find before closing
+ // the outer boundary.
+ // We exclude the open and closing marker from
+ // the future excessDomChildren but any nested one
+ // needs to be included for future suspensions.
+ if (child.nodeType == 8 && child.data == '$s') {
+ if (commentMarkersToFind > 0) {
+ newVNode._component._excess.push(child);
+ }
+ commentMarkersToFind++;
+ excessDomChildren[i] = NULL;
+ } else if (child.nodeType == 8 && child.data == '/$s') {
+ commentMarkersToFind--;
+ if (commentMarkersToFind > 0) {
+ newVNode._component._excess.push(child);
+ }
+ done = commentMarkersToFind === 0;
+ oldDom = excessDomChildren[i];
+ excessDomChildren[i] = NULL;
+ } else if (commentMarkersToFind > 0) {
+ newVNode._component._excess.push(child);
+ excessDomChildren[i] = NULL;
+ }
+ }
+
+ if (!done) {
+ while (oldDom && oldDom.nodeType == 8 && oldDom.nextSibling) {
+ oldDom = oldDom.nextSibling;
+ }
+
+ excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
+ newVNode._component._excess = [oldDom];
}
- excessDomChildren[excessDomChildren.indexOf(oldDom)] = NULL;
newVNode._dom = oldDom;
} else {
for (let i = excessDomChildren.length; i--; ) {
@@ -316,12 +371,6 @@ export function diff(
}
options._catchError(e, newVNode, oldVNode);
}
- } else if (
- excessDomChildren == NULL &&
- newVNode._original == oldVNode._original
- ) {
- newVNode._children = oldVNode._children;
- newVNode._dom = oldVNode._dom;
} else {
oldDom = newVNode._dom = diffElementNodes(
oldVNode._dom,
@@ -580,12 +629,7 @@ function diffElementNodes(
// despite the attribute not being present. When the attribute
// is missing the progress bar is treated as indeterminate.
// To fix that we'll always update it when it is 0 for progress elements
- (inputValue !== dom[i] ||
- (nodeType == 'progress' && !inputValue) ||
- // This is only for IE 11 to fix value not being updated.
- // To avoid a stale select value we need to set the option.value
- // again, which triggers IE11 to re-evaluate the select value
- (nodeType == 'option' && inputValue != oldProps[i]))
+ (inputValue !== dom[i] || (nodeType === 'progress' && !inputValue))
) {
setProperty(dom, i, inputValue, oldProps[i], namespace);
}
@@ -653,7 +697,7 @@ export function unmount(vnode, parentVNode, skipRemove) {
}
}
- r.base = r._parentDom = NULL;
+ r._parentDom = NULL;
}
if ((r = vnode._children)) {
diff --git a/src/diff/props.js b/src/diff/props.js
index 31bbb5daf5..ed17f763fa 100644
--- a/src/diff/props.js
+++ b/src/diff/props.js
@@ -1,4 +1,4 @@
-import { IS_NON_DIMENSIONAL, NULL, SVG_NAMESPACE } from '../constants';
+import { NULL, SVG_NAMESPACE } from '../constants';
import options from '../options';
function setStyle(style, key, value) {
@@ -6,10 +6,8 @@ function setStyle(style, key, value) {
style.setProperty(key, value == NULL ? '' : value);
} else if (value == NULL) {
style[key] = '';
- } else if (typeof value != 'number' || IS_NON_DIMENSIONAL.test(key)) {
- style[key] = value;
} else {
- style[key] = value + 'px';
+ style[key] = value;
}
}
@@ -67,11 +65,9 @@ export function setProperty(dom, name, value, oldValue, namespace) {
// Benchmark for comparison: https://esbench.com/bench/574c954bdb965b9a00965ac6
else if (name[0] == 'o' && name[1] == 'n') {
useCapture = name != (name = name.replace(CAPTURE_REGEX, '$1'));
- const lowerCaseName = name.toLowerCase();
// Infer correct casing for DOM built-in events:
- if (lowerCaseName in dom || name == 'onFocusOut' || name == 'onFocusIn')
- name = lowerCaseName.slice(2);
+ if (name[2].toLowerCase() != name[2]) name = name.toLowerCase().slice(2);
else name = name.slice(2);
if (!dom._listeners) dom._listeners = {};
diff --git a/src/index-5.d.ts b/src/index-5.d.ts
index 6c7431cb00..1431b01f30 100644
--- a/src/index-5.d.ts
+++ b/src/index-5.d.ts
@@ -89,14 +89,12 @@ export type ComponentProps<
export interface FunctionComponent {
(props: RenderableProps
, context?: any): VNode | null;
displayName?: string;
- defaultProps?: Partial
| undefined;
}
export interface FunctionalComponent
extends FunctionComponent
{}
export interface ComponentClass
{
new (props: P, context?: any): Component
;
displayName?: string;
- defaultProps?: Partial
;
contextType?: Context;
getDerivedStateFromProps?(
props: Readonly,
@@ -141,7 +139,6 @@ export abstract class Component
{
constructor(props?: P, context?: any);
static displayName?: string;
- static defaultProps?: any;
static contextType?: Context;
// Static members cannot reference class type parameters. This is not
@@ -293,7 +290,6 @@ interface ContainerNode {
readonly firstChild: ContainerNode | null;
readonly childNodes: ArrayLike;
- contains(other: ContainerNode | null): boolean;
insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode;
appendChild(node: ContainerNode): ContainerNode;
removeChild(child: ContainerNode): ContainerNode;
@@ -348,6 +344,7 @@ export interface Options {
debounceRendering?(cb: () => void): void;
useDebugValue?(value: string | number): void;
_addHookName?(name: string | number): void;
+ _afterRender?(newVNode: VNode, oldVNode: VNode): void;
__suspenseDidResolve?(vnode: VNode, cb: () => void): void;
// __canSuspenseResolve?(vnode: VNode, cb: () => void): void;
diff --git a/src/index.d.ts b/src/index.d.ts
index 379bbebaaf..e6b8fd549e 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -37,9 +37,9 @@ export interface VNode {
export type Key = string | number | any;
-export type RefObject = { current: T | null };
+export type RefObject = { current: T };
export type RefCallback = (instance: T | null) => void;
-export type Ref = RefObject | RefCallback | null;
+export type Ref = RefCallback | RefObject | null;
export type ComponentChild =
| VNode
@@ -89,14 +89,12 @@ export type ComponentProps<
export interface FunctionComponent {
(props: RenderableProps
, context?: any): ComponentChildren;
displayName?: string;
- defaultProps?: Partial
| undefined;
}
export interface FunctionalComponent
extends FunctionComponent
{}
export interface ComponentClass
{
new (props: P, context?: any): Component
;
displayName?: string;
- defaultProps?: Partial
;
contextType?: Context;
getDerivedStateFromProps?(
props: Readonly,
@@ -141,7 +139,6 @@ export abstract class Component
{
constructor(props?: P, context?: any);
static displayName?: string;
- static defaultProps?: any;
static contextType?: Context;
// Static members cannot reference class type parameters. This is not
@@ -159,7 +156,6 @@ export abstract class Component {
state: Readonly;
props: RenderableProps
;
context: any;
- base?: Element | Text;
// From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402
// // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
@@ -293,23 +289,12 @@ interface ContainerNode {
readonly firstChild: ContainerNode | null;
readonly childNodes: ArrayLike;
- contains(other: ContainerNode | null): boolean;
insertBefore(node: ContainerNode, child: ContainerNode | null): ContainerNode;
appendChild(node: ContainerNode): ContainerNode;
removeChild(child: ContainerNode): ContainerNode;
}
export function render(vnode: ComponentChild, parent: ContainerNode): void;
-/**
- * @deprecated Will be removed in v11.
- *
- * Replacement Preact 10+ implementation can be found here: https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
- */
-export function render(
- vnode: ComponentChild,
- parent: ContainerNode,
- replaceNode?: Element | Text
-): void;
export function hydrate(vnode: ComponentChild, parent: ContainerNode): void;
export function cloneElement(
vnode: VNode,
diff --git a/src/internal.d.ts b/src/internal.d.ts
index 85e704be65..403fe37dbb 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -27,6 +27,8 @@ export interface ErrorInfo {
}
export interface Options extends preact.Options {
+ /** Attach a hook that is invoked after a vnode has rendered. */
+ _afterRender?(vnode: VNode, oldVNode: VNode): void;
/** Attach a hook that is invoked before render, mainly to check the arguments. */
_root?(vnode: ComponentChild, parent: preact.ContainerNode): void;
/** Attach a hook that is invoked before a vnode is diffed. */
@@ -62,8 +64,7 @@ export type ComponentChild =
| undefined;
export type ComponentChildren = ComponentChild[] | ComponentChild;
-export interface FunctionComponent
- extends preact.FunctionComponent
{
+export interface FunctionComponent
extends preact.FunctionComponent
{
// Internally, createContext uses `contextType` on a Function component to
// implement the Consumer component
contextType?: PreactContext;
@@ -95,6 +96,7 @@ export interface PreactElement extends preact.ContainerNode {
data?: CharacterData['data'];
// Property to set __dangerouslySetInnerHTML
innerHTML?: Element['innerHTML'];
+ remove?: Element['remove'];
// Attribute reading and setting
readonly attributes?: Element['attributes'];
@@ -139,9 +141,7 @@ type RefCallback = {
export type Ref = RefObject | RefCallback;
export interface VNode extends preact.VNode
{
- // Redefine type here using our internal ComponentType type, and specify
- // string has an undefined `defaultProps` property to make TS happy
- type: (string & { defaultProps: undefined }) | ComponentType
;
+ type: string | ComponentType
;
props: P & { children: ComponentChildren };
ref?: Ref | null;
_children: Array> | null;
@@ -158,12 +158,13 @@ export interface VNode extends preact.VNode
{
_flags: number;
}
-export interface Component
extends Omit, 'base'> {
+export interface Component
+ extends Omit, 'base'> {
// When component is functional component, this is reset to functional component
constructor: ComponentType;
state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks
- base?: PreactElement;
+ _excess?: PreactElement[];
_dirty: boolean;
_force?: boolean;
_renderCallbacks: Array<() => void>; // Only class components
diff --git a/src/jsx.d.ts b/src/jsx.d.ts
index 0cf3c6a79b..ad17309c2d 100644
--- a/src/jsx.d.ts
+++ b/src/jsx.d.ts
@@ -1552,6 +1552,8 @@ export namespace JSXInternal {
translate?: Signalish;
// WAI-ARIA Attributes
+ // Most elements only allow a subset of roles and so this
+ // is overwritten in many of the per-element interfaces below
role?: Signalish;
// Non-standard Attributes
@@ -1601,10 +1603,9 @@ export namespace JSXInternal {
| '_top'
| (string & {});
- interface AnchorHTMLAttributes
+ interface PartialAnchorHTMLAttributes
extends HTMLAttributes {
download?: Signalish;
- href?: Signalish;
hreflang?: Signalish;
hrefLang?: Signalish;
media?: Signalish;
@@ -1616,12 +1617,44 @@ export namespace JSXInternal {
referrerPolicy?: Signalish;
}
- interface AreaHTMLAttributes
+ type AnchorAriaRoles =
+ | {
+ href: Signalish;
+ role?: Signalish<
+ | 'link'
+ | 'button'
+ | 'checkbox'
+ | 'menuitem'
+ | 'menuitemcheckbox'
+ | 'menuitemradio'
+ | 'option'
+ | 'radio'
+ | 'switch'
+ | 'tab'
+ | 'treeitem'
+ | 'doc-backlink'
+ | 'doc-biblioref'
+ | 'doc-glossref'
+ | 'doc-noteref'
+ | undefined
+ >;
+ }
+ | {
+ href?: never;
+ role?: Signalish;
+ };
+
+ type AnchorHTMLAttributes = Omit<
+ PartialAnchorHTMLAttributes,
+ 'role'
+ > &
+ AnchorAriaRoles;
+
+ interface PartialAreaHTMLAttributes
extends HTMLAttributes {
alt?: Signalish;
coords?: Signalish;
download?: Signalish;
- href?: Signalish;
hreflang?: Signalish;
hrefLang?: Signalish;
media?: Signalish;
@@ -1632,12 +1665,66 @@ export namespace JSXInternal {
target?: Signalish;
}
+ type AreaAriaRoles =
+ | {
+ href: Signalish;
+ role?: Signalish<'link' | undefined>;
+ }
+ | {
+ href?: never;
+ role?: Signalish<'button' | 'link' | undefined>;
+ };
+
+ type AreaHTMLAttributes = Omit<
+ PartialAreaHTMLAttributes,
+ 'role'
+ > &
+ AreaAriaRoles;
+
+ interface ArticleHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<
+ | 'article'
+ | 'application'
+ | 'document'
+ | 'feed'
+ | 'main'
+ | 'none'
+ | 'presentation'
+ | 'region'
+ | undefined
+ >;
+ }
+
+ interface AsideHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<
+ | 'complementary'
+ | 'feed'
+ | 'none'
+ | 'note'
+ | 'presentation'
+ | 'region'
+ | 'search'
+ | 'doc-dedication'
+ | 'doc-example'
+ | 'doc-footnote'
+ | 'doc-glossary'
+ | 'doc-pullquote'
+ | 'doc-tip'
+ | undefined
+ >;
+ }
+
interface AudioHTMLAttributes
- extends MediaHTMLAttributes {}
+ extends MediaHTMLAttributes {
+ role?: Signalish<'application' | undefined>;
+ }
interface BaseHTMLAttributes
extends HTMLAttributes {
href?: Signalish;
+ role?: never;
target?: Signalish;
}
@@ -1646,6 +1733,11 @@ export namespace JSXInternal {
cite?: Signalish;
}
+ interface BrHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'none' | 'presentation' | undefined>;
+ }
+
interface ButtonHTMLAttributes
extends HTMLAttributes {
command?: Signalish;
@@ -1668,6 +1760,24 @@ export namespace JSXInternal {
popoverTarget?: Signalish;
popovertargetaction?: Signalish<'hide' | 'show' | 'toggle' | undefined>;
popoverTargetAction?: Signalish<'hide' | 'show' | 'toggle' | undefined>;
+ role?: Signalish<
+ | 'button'
+ | 'checkbox'
+ | 'combobox'
+ | 'gridcell'
+ | 'link'
+ | 'menuitem'
+ | 'menuitemcheckbox'
+ | 'menuitemradio'
+ | 'option'
+ | 'radio'
+ | 'separator'
+ | 'slider'
+ | 'switch'
+ | 'tab'
+ | 'treeitem'
+ | undefined
+ >;
type?: Signalish<'submit' | 'reset' | 'button' | undefined>;
value?: Signalish;
}
@@ -1678,14 +1788,21 @@ export namespace JSXInternal {
width?: Signalish;
}
+ interface CaptionHTMLAttributes
+ extends HTMLAttributes {
+ role?: 'caption';
+ }
+
interface ColHTMLAttributes
extends HTMLAttributes {
+ role?: never;
span?: Signalish;
width?: Signalish;
}
interface ColgroupHTMLAttributes
extends HTMLAttributes {
+ role?: never;
span?: Signalish;
}
@@ -1694,6 +1811,16 @@ export namespace JSXInternal {
value?: Signalish;
}
+ interface DataListHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'listbox' | undefined>;
+ }
+
+ interface DdHTMLAttributes
+ extends HTMLAttributes {
+ role?: never;
+ }
+
interface DelHTMLAttributes
extends HTMLAttributes {
cite?: Signalish;
@@ -1705,6 +1832,7 @@ export namespace JSXInternal {
extends HTMLAttributes {
name?: Signalish;
open?: Signalish;
+ role?: Signalish<'group' | undefined>;
}
interface DialogHTMLAttributes
@@ -1714,11 +1842,25 @@ export namespace JSXInternal {
open?: Signalish;
closedby?: Signalish<'none' | 'closerequest' | 'any' | undefined>;
closedBy?: Signalish<'none' | 'closerequest' | 'any' | undefined>;
+ role?: Signalish<'dialog' | 'alertdialog' | undefined>;
+ }
+
+ interface DlHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'group' | 'list' | 'none' | 'presentation' | undefined>;
+ }
+
+ interface DtHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'listitem' | undefined>;
}
interface EmbedHTMLAttributes
extends HTMLAttributes {
height?: Signalish;
+ role?: Signalish<
+ 'application' | 'document' | 'img' | 'none' | 'presentation' | undefined
+ >;
src?: Signalish;
type?: Signalish;
width?: Signalish;
@@ -1729,6 +1871,26 @@ export namespace JSXInternal {
disabled?: Signalish;
form?: Signalish;
name?: Signalish;
+ role?: Signalish<
+ 'group' | 'none' | 'presentation' | 'radiogroup' | undefined
+ >;
+ }
+
+ interface FigcaptionHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'group' | 'none' | 'presentation' | undefined>;
+ }
+
+ interface FooterHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<
+ | 'contentinfo'
+ | 'group'
+ | 'none'
+ | 'presentation'
+ | 'doc-footnote'
+ | undefined
+ >;
}
interface FormHTMLAttributes
@@ -1745,9 +1907,39 @@ export namespace JSXInternal {
novalidate?: Signalish;
noValidate?: Signalish;
rel?: Signalish;
+ role?: Signalish<'form' | 'none' | 'presentation' | 'search' | undefined>;
target?: Signalish;
}
+ interface HeadingHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<
+ 'heading' | 'none' | 'presentation' | 'tab' | 'doc-subtitle' | undefined
+ >;
+ }
+
+ interface HeadHTMLAttributes
+ extends HTMLAttributes {
+ role?: never;
+ }
+
+ interface HeaderHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'banner' | 'group' | 'none' | 'presentation' | undefined>;
+ }
+
+ interface HrHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<
+ 'separator' | 'none' | 'presentation' | 'doc-pagebreak' | undefined
+ >;
+ }
+
+ interface HtmlHTMLAttributes
+ extends HTMLAttributes {
+ role?: Signalish<'document' | undefined>;
+ }
+
interface IframeHTMLAttributes
extends HTMLAttributes {
allow?: Signalish;
@@ -1766,6 +1958,9 @@ export namespace JSXInternal {
name?: Signalish;
referrerpolicy?: Signalish;
referrerPolicy?: Signalish;
+ role?: Signalish<
+ 'application' | 'document' | 'img' | 'none' | 'presentation' | undefined
+ >;
sandbox?: Signalish;
/** @deprecated */
scrolling?: Signalish;
@@ -1778,9 +1973,8 @@ export namespace JSXInternal {
type HTMLAttributeCrossOrigin = 'anonymous' | 'use-credentials';
- interface ImgHTMLAttributes
+ interface PartialImgHTMLAttributes