From c5f048366a8a02417125eb17829f472122938b82 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sat, 2 Mar 2024 10:05:57 +0100 Subject: [PATCH 01/15] update some actions to remove node 20 warnings (#4298) --- .github/workflows/benchmarks.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 9c6624f674..51f0ce9a4c 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -26,12 +26,12 @@ jobs: timeout-minutes: 5 steps: - name: Download locally built preact package - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: npm-package - run: mv preact.tgz preact-local.tgz - name: Upload locally built preact package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bench-environment path: preact-local.tgz diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b32ee79ea0..ef15be011b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: npm pack --ignore-scripts mv preact-*.tgz preact.tgz - name: Upload npm package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ inputs.artifact_name || 'npm-package' }} path: preact.tgz From 1584d4cef6e51b33b0bf3337178db7144f69f91e Mon Sep 17 00:00:00 2001 From: Rodrigo Leal Date: Wed, 13 Mar 2024 20:15:50 -0300 Subject: [PATCH 02/15] feat: added isMemo to compact to allow compatibility with react-is dependant libraries --- compat/src/index.d.ts | 16 +++++++------ compat/src/index.js | 17 ++++++++++++++ compat/test/browser/isMemo.test.js | 37 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 compat/test/browser/isMemo.test.js diff --git a/compat/src/index.d.ts b/compat/src/index.d.ts index 80a930d024..3a054da47c 100644 --- a/compat/src/index.d.ts +++ b/compat/src/index.d.ts @@ -115,14 +115,16 @@ declare namespace React { ) => preact.VNode; export function isValidElement(element: any): boolean; export function isFragment(element: any): boolean; + export function isMemo(element: any): boolean; export function findDOMNode( component: preact.Component | Element ): Element | null; - export abstract class PureComponent

extends preact.Component< - P, - S - > { + export abstract class PureComponent< + P = {}, + S = {}, + SS = any + > extends preact.Component { isPureReactComponent: boolean; } @@ -174,9 +176,9 @@ declare namespace React { export type ComponentPropsWithRef< C extends ComponentType | keyof JSXInternal.IntrinsicElements - > = C extends (new(props: infer P) => Component) - ? PropsWithoutRef

& RefAttributes> - : ComponentProps; + > = C extends new (props: infer P) => Component + ? PropsWithoutRef

& RefAttributes> + : ComponentProps; export function flushSync(fn: () => R): R; export function flushSync(fn: (a: A) => R, a: A): R; diff --git a/compat/src/index.js b/compat/src/index.js index 830d34dd24..f08b89b03d 100644 --- a/compat/src/index.js +++ b/compat/src/index.js @@ -63,6 +63,21 @@ function isFragment(element) { return isValidElement(element) && element.type === Fragment; } +/** + * Check if the passed element is a Memo node. + * @param {*} element The element to check + * @returns {boolean} + */ +function isMemo(element) { + return ( + !!element && + !!element.displayName && + (typeof element.displayName === 'string' || + element.displayName instanceof String) && + element.displayName.startsWith('Memo(') + ); +} + /** * Wrap `cloneElement` to abort if the passed element is not a valid element and apply * all vnode normalizations. @@ -215,6 +230,7 @@ export { Fragment, isValidElement, isFragment, + isMemo, findDOMNode, Component, PureComponent, @@ -263,6 +279,7 @@ export default { isValidElement, isElement, isFragment, + isMemo, findDOMNode, Component, PureComponent, diff --git a/compat/test/browser/isMemo.test.js b/compat/test/browser/isMemo.test.js new file mode 100644 index 0000000000..a11c1a9823 --- /dev/null +++ b/compat/test/browser/isMemo.test.js @@ -0,0 +1,37 @@ +import { createElement as preactCreateElement, Fragment } from 'preact'; +import React, { createElement, isMemo, memo } from 'preact/compat'; + +describe('isMemo', () => { + it('should check return false for invalid arguments', () => { + expect(isMemo(null)).to.equal(false); + expect(isMemo(false)).to.equal(false); + expect(isMemo(true)).to.equal(false); + expect(isMemo('foo')).to.equal(false); + expect(isMemo(123)).to.equal(false); + expect(isMemo([])).to.equal(false); + expect(isMemo({})).to.equal(false); + }); + + it('should detect a preact memo', () => { + function Foo() { + return

Hello World

; + } + let App = memo(Foo); + expect(isMemo(App)).to.equal(true); + }); + + it('should not detect a normal element', () => { + function Foo() { + return

Hello World

; + } + expect(isMemo(Foo)).to.equal(false); + }); + + it('should detect a preact vnode as false', () => { + expect(isMemo(preactCreateElement(Fragment, {}))).to.equal(false); + }); + + it('should detect a compat vnode as false', () => { + expect(isMemo(React.createElement(Fragment, {}))).to.equal(false); + }); +}); From dc5584182aecedb18ad44095caa525a0569d8390 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 17 Mar 2024 15:45:19 +0100 Subject: [PATCH 03/15] Revert "Merge pull request #4234 from preactjs/multi-root-shared-commit" (#4297) This reverts commit f7ccb9010077ecb46fc271224bbc5e015e00efe6, reversing changes made to e1f0d3e626d6ebc76a2851e830c5b6020c353cf2. --- .../test/browser/useSyncExternalStore.test.js | 5 +++- src/component.js | 29 +++++-------------- src/diff/index.js | 2 ++ src/render.js | 2 +- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/compat/test/browser/useSyncExternalStore.test.js b/compat/test/browser/useSyncExternalStore.test.js index c51760cd24..2f5ab16a03 100644 --- a/compat/test/browser/useSyncExternalStore.test.js +++ b/compat/test/browser/useSyncExternalStore.test.js @@ -658,7 +658,10 @@ describe('useSyncExternalStore', () => { await act(() => { store.set(1); }); - assertLog([1, 1, 'Reset back to 0', 0, 0]); + // Preact logs differ from React here cuz of how we do rerendering. We + // rerender subtrees and then commit effects so Child2 never sees the + // update to 1 cuz Child1 rerenders and runs its layout effects first. + assertLog([1, /*1,*/ 'Reset back to 0', 0, 0]); expect(container.textContent).to.equal('00'); }); diff --git a/src/component.js b/src/component.js index d38f60ca36..62b2439f72 100644 --- a/src/component.js +++ b/src/component.js @@ -2,7 +2,7 @@ import { assign } from './util'; import { diff, commitRoot } from './diff/index'; import options from './options'; import { Fragment } from './create-element'; -import { EMPTY_ARR, MODE_HYDRATE } from './constants'; +import { MODE_HYDRATE } from './constants'; /** * Base Component class. Provides `setState()` and `forceUpdate()`, which @@ -120,10 +120,12 @@ export function getDomSibling(vnode, childIndex) { * Trigger in-place re-rendering of a component. * @param {Component} component The component to rerender */ -function renderComponent(component, commitQueue, refQueue) { +function renderComponent(component) { let oldVNode = component._vnode, oldDom = oldVNode._dom, - parentDom = component._parentDom; + parentDom = component._parentDom, + commitQueue = [], + refQueue = []; if (parentDom) { const newVNode = assign({}, oldVNode); @@ -145,14 +147,11 @@ function renderComponent(component, commitQueue, refQueue) { newVNode._original = oldVNode._original; newVNode._parent._children[newVNode._index] = newVNode; - - newVNode._nextDom = undefined; + commitRoot(commitQueue, newVNode, refQueue); if (newVNode._dom != oldDom) { updateParentDomPointers(newVNode); } - - return newVNode; } } @@ -222,33 +221,21 @@ const depthSort = (a, b) => a._vnode._depth - b._vnode._depth; /** Flush the render queue by rerendering all queued components */ function process() { let c; - let commitQueue = []; - let refQueue = []; - let root; rerenderQueue.sort(depthSort); // Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary // process() calls from getting scheduled while `queue` is still being consumed. while ((c = rerenderQueue.shift())) { if (c._dirty) { let renderQueueLength = rerenderQueue.length; - root = renderComponent(c, commitQueue, refQueue) || root; - // If this WAS the last component in the queue, run commit callbacks *before* we exit the tight loop. - // This is required in order for `componentDidMount(){this.setState()}` to be batched into one flush. - // Otherwise, also run commit callbacks if the render queue was mutated. - if (renderQueueLength === 0 || rerenderQueue.length > renderQueueLength) { - commitRoot(commitQueue, root, refQueue); - refQueue.length = commitQueue.length = 0; - root = undefined; + renderComponent(c); + if (rerenderQueue.length > renderQueueLength) { // When i.e. rerendering a provider additional new items can be injected, we want to // keep the order from top to bottom with those new items so we can handle them in a // single pass rerenderQueue.sort(depthSort); - } else if (root) { - if (options._commit) options._commit(root, EMPTY_ARR); } } } - if (root) commitRoot(commitQueue, root, refQueue); process._rerenderCount = 0; } diff --git a/src/diff/index.js b/src/diff/index.js index 16b07395a2..b5bbb49db6 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -311,6 +311,8 @@ export function diff( * @param {VNode} root */ export function commitRoot(commitQueue, root, refQueue) { + root._nextDom = undefined; + for (let i = 0; i < refQueue.length; i++) { applyRef(refQueue[i], refQueue[++i], refQueue[++i]); } diff --git a/src/render.js b/src/render.js index b550ef96af..1ee326bc92 100644 --- a/src/render.js +++ b/src/render.js @@ -59,7 +59,7 @@ export function render(vnode, parentDom, replaceNode) { refQueue ); - vnode._nextDom = undefined; + // Flush all queued effects commitRoot(commitQueue, vnode, refQueue); } From f8a080180b3b71d4229e09eb2e2df5e7d683ffd3 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 17 Mar 2024 19:32:06 +0100 Subject: [PATCH 04/15] align state updater type (#4306) --- hooks/src/index.d.ts | 12 ++++++++---- hooks/src/index.js | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/hooks/src/index.d.ts b/hooks/src/index.d.ts index 561f034943..3ec8999ca6 100644 --- a/hooks/src/index.d.ts +++ b/hooks/src/index.d.ts @@ -2,20 +2,24 @@ import { ErrorInfo, PreactContext, Ref as PreactRef } from '../..'; type Inputs = ReadonlyArray; -export type StateUpdater = (value: S | ((prevState: S) => S)) => void; +export type Dispatch = (value: A) => void; +export type StateUpdater = S | ((prevState: S) => S); + /** * Returns a stateful value, and a function to update it. * @param initialState The initial value (or a function that returns the initial value) */ -export function useState(initialState: S | (() => S)): [S, StateUpdater]; +export function useState( + initialState: S | (() => S) +): [S, Dispatch>]; export function useState(): [ S | undefined, - StateUpdater + Dispatch> ]; export type Reducer = (prevState: S, action: A) => S; -export type Dispatch = (action: A) => void; + /** * An alternative to `useState`. * diff --git a/hooks/src/index.js b/hooks/src/index.js index f2a0c12922..094410d7a3 100644 --- a/hooks/src/index.js +++ b/hooks/src/index.js @@ -167,7 +167,7 @@ function getHookState(index, type) { /** * @template {unknown} S - * @param {import('./index').StateUpdater} [initialState] + * @param {import('./index').Dispatch>} [initialState] * @returns {[S, (state: S) => void]} */ export function useState(initialState) { @@ -179,7 +179,7 @@ export function useState(initialState) { * @template {unknown} S * @template {unknown} A * @param {import('./index').Reducer} reducer - * @param {import('./index').StateUpdater} initialState + * @param {import('./index').Dispatch>} initialState * @param {(initialState: any) => void} [init] * @returns {[ S, (state: S) => void ]} */ From cfba5eef8e4072f6d01594386b34b8902d3c6ada Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 17 Mar 2024 19:39:00 +0100 Subject: [PATCH 05/15] add onscrollend listener type (#4305) --- src/jsx.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/jsx.d.ts b/src/jsx.d.ts index c58ac48e69..24aa73beae 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -1718,6 +1718,7 @@ export namespace JSXInternal { // UI Events onScroll?: UIEventHandler | undefined; + onScrollEnd?: UIEventHandler | undefined; onScrollCapture?: UIEventHandler | undefined; // Wheel Events From a2c12f5a46a70b2b58517f5e14e731a77d6d64a3 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sun, 17 Mar 2024 19:40:38 +0100 Subject: [PATCH 06/15] bring consistency to our focus events (#4307) --- src/jsx.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jsx.d.ts b/src/jsx.d.ts index 24aa73beae..2accfbac89 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -1563,10 +1563,10 @@ export namespace JSXInternal { // Focus Events onFocus?: FocusEventHandler | undefined; onFocusCapture?: FocusEventHandler | undefined; - onfocusin?: FocusEventHandler | undefined; - onfocusinCapture?: FocusEventHandler | undefined; - onfocusout?: FocusEventHandler | undefined; - onfocusoutCapture?: FocusEventHandler | undefined; + onFocusIn?: FocusEventHandler | undefined; + onFocusInCapture?: FocusEventHandler | undefined; + onFocusOut?: FocusEventHandler | undefined; + onFocusOutCapture?: FocusEventHandler | undefined; onBlur?: FocusEventHandler | undefined; onBlurCapture?: FocusEventHandler | undefined; From b2ff0560e9270b7786e6be385818d9c84d8e113a Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 18 Mar 2024 16:33:56 +0100 Subject: [PATCH 07/15] try to fix benchmarks (#4304) * try to fix benchmarks * empty commit test * add overwrite * Revert "update some actions to remove node 20 warnings (#4298)" This reverts commit c5f048366a8a02417125eb17829f472122938b82. * Revert "add overwrite" This reverts commit cfce3d2de3bc3638694b2c4ffde76e8e420fe764. * Revert "try to fix benchmarks" This reverts commit cb13cd24a959a5860e3db3fc6d868bfa1a4017e8. * test comment * remove comment forcing bench runs --- .github/workflows/benchmarks.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 51f0ce9a4c..9c6624f674 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -26,12 +26,12 @@ jobs: timeout-minutes: 5 steps: - name: Download locally built preact package - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v3 with: name: npm-package - run: mv preact.tgz preact-local.tgz - name: Upload locally built preact package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: bench-environment path: preact-local.tgz diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef15be011b..b32ee79ea0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: npm pack --ignore-scripts mv preact-*.tgz preact.tgz - name: Upload npm package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v3 with: name: ${{ inputs.artifact_name || 'npm-package' }} path: preact.tgz From e1747136e829f3cdd249a731e0143eedef1f1cd6 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 18 Mar 2024 16:40:14 +0100 Subject: [PATCH 08/15] 10.19.7 (#4308) --- devtools/src/devtools.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/src/devtools.js b/devtools/src/devtools.js index 2bc6e72ff1..9126e904c0 100644 --- a/devtools/src/devtools.js +++ b/devtools/src/devtools.js @@ -2,7 +2,7 @@ import { options, Fragment, Component } from 'preact'; export function initDevTools() { if (typeof window != 'undefined' && window.__PREACT_DEVTOOLS__) { - window.__PREACT_DEVTOOLS__.attachPreact('10.19.6', options, { + window.__PREACT_DEVTOOLS__.attachPreact('10.19.7', options, { Fragment, Component }); diff --git a/package-lock.json b/package-lock.json index 3582cb76ec..340a34fcb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "preact", - "version": "10.19.6", + "version": "10.19.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "preact", - "version": "10.19.6", + "version": "10.19.7", "license": "MIT", "devDependencies": { "@actions/github": "^5.0.0", diff --git a/package.json b/package.json index fe32b22d52..c78a5cf8d3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "preact", "amdName": "preact", - "version": "10.19.6", + "version": "10.19.7", "private": false, "description": "Fast 3kb React-compatible Virtual DOM library.", "main": "dist/preact.js", From 342b50e8624ef6566abc844b725a09ef4aa4b390 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 19 Mar 2024 19:13:46 +0100 Subject: [PATCH 09/15] fix: case where shrinking a list would cause an exception (#4312) * yikes * fix issue --- src/diff/children.js | 4 ++++ test/browser/render.test.js | 48 +++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/diff/children.js b/src/diff/children.js index b44d5168e8..eba002d56d 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -116,6 +116,9 @@ export function diffChildren( childVNode._flags & INSERT_VNODE || oldVNode._children === childVNode._children ) { + if (!newDom && oldVNode._dom == oldDom) { + oldDom = getDomSibling(oldVNode); + } oldDom = insert(childVNode, oldDom, parentDom); } else if ( typeof childVNode.type == 'function' && @@ -241,6 +244,7 @@ function constructNewChildrenArray(newParentVNode, renderResult, oldChildren) { if (oldVNode._dom == newParentVNode._nextDom) { newParentVNode._nextDom = getDomSibling(oldVNode); } + unmount(oldVNode, oldVNode, false); // Explicitly nullify this position in oldChildren instead of just diff --git a/test/browser/render.test.js b/test/browser/render.test.js index ae42ce2720..822f864f21 100644 --- a/test/browser/render.test.js +++ b/test/browser/render.test.js @@ -1481,8 +1481,8 @@ describe('render()', () => { expect(serializeHtml(scratch)).to.equal( '

_B1

_B2

_B3

_B4

_B5

_B6

_B7

_B8

_B9

_B10

_B11

_B12

_B13

' - ); - }); + ); + }); it('should not crash or repeatedly add the same child when replacing a matched vnode with null (mixed dom-types)', () => { const B = () =>
B
; @@ -1537,4 +1537,48 @@ describe('render()', () => { '
A
B
C
' ); }); + + it('should shrink lists', () => { + function RenderedItem({ item }) { + if (item.renderAsNullInComponent) { + return null; + } + + return
{item.id}
; + } + + function App({ list }) { + return ( +
+ {list.map(item => ( + + ))} +
+ ); + } + + const firstList = [ + { id: 'One' }, + { id: 'Two' }, + { id: 'Three' }, + { id: 'Four' } + ]; + + const secondList = [ + { id: 'One' }, + { id: 'Four', renderAsNullInComponent: true }, + { id: 'Six' }, + { id: 'Seven' } + ]; + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
One
Two
Three
Four
' + ); + + render(, scratch); + expect(scratch.innerHTML).to.equal( + '
One
Six
Seven
' + ); + }); }); From b820d8b73eade8bc5b3fbf7e5fe0dd5150de80e2 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Wed, 20 Mar 2024 07:45:35 +0100 Subject: [PATCH 10/15] 10.20.0 (#4313) * 10.19.8 * 10.20.0 --- devtools/src/devtools.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/src/devtools.js b/devtools/src/devtools.js index 9126e904c0..ef30f48226 100644 --- a/devtools/src/devtools.js +++ b/devtools/src/devtools.js @@ -2,7 +2,7 @@ import { options, Fragment, Component } from 'preact'; export function initDevTools() { if (typeof window != 'undefined' && window.__PREACT_DEVTOOLS__) { - window.__PREACT_DEVTOOLS__.attachPreact('10.19.7', options, { + window.__PREACT_DEVTOOLS__.attachPreact('10.20.0', options, { Fragment, Component }); diff --git a/package-lock.json b/package-lock.json index 340a34fcb7..29e49d78f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "preact", - "version": "10.19.7", + "version": "10.20.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "preact", - "version": "10.19.7", + "version": "10.20.0", "license": "MIT", "devDependencies": { "@actions/github": "^5.0.0", diff --git a/package.json b/package.json index c78a5cf8d3..46db7c191b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "preact", "amdName": "preact", - "version": "10.19.7", + "version": "10.20.0", "private": false, "description": "Fast 3kb React-compatible Virtual DOM library.", "main": "dist/preact.js", From a59a78ce14879bb57ff8269522f9151ccee09db7 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Thu, 21 Mar 2024 12:52:34 +0100 Subject: [PATCH 11/15] fix focus in and out (#4316) --- src/diff/props.js | 7 ++++++- test/browser/events.test.js | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/diff/props.js b/src/diff/props.js index 75016c4025..142e1444c9 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -55,7 +55,12 @@ export function setProperty(dom, name, value, oldValue, isSvg) { name !== (name = name.replace(/(PointerCapture)$|Capture$/i, '$1')); // Infer correct casing for DOM built-in events: - if (name.toLowerCase() in dom) name = name.toLowerCase().slice(2); + if ( + name.toLowerCase() in dom || + name === 'onFocusOut' || + name === 'onFocusIn' + ) + name = name.toLowerCase().slice(2); else name = name.slice(2); if (!dom._listeners) dom._listeners = {}; diff --git a/test/browser/events.test.js b/test/browser/events.test.js index 8a2732adf9..ef4f990e49 100644 --- a/test/browser/events.test.js +++ b/test/browser/events.test.js @@ -227,4 +227,12 @@ describe('event handling', () => { .to.have.been.calledTwice.and.to.have.been.calledWith('gotpointercapture') .and.calledWith('lostpointercapture'); }); + + it('should support camel-case focus event names', () => { + render(
{}} onFocusOut={() => {}} />, scratch); + + expect(proto.addEventListener) + .to.have.been.calledTwice.and.to.have.been.calledWith('focusin') + .and.calledWith('focusout'); + }); }); From bc7c5515599634a427334eb03236ddf19e13e618 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Sat, 23 Mar 2024 07:46:25 +0100 Subject: [PATCH 12/15] 10.20.1 (#4319) --- devtools/src/devtools.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/src/devtools.js b/devtools/src/devtools.js index ef30f48226..078e309f0b 100644 --- a/devtools/src/devtools.js +++ b/devtools/src/devtools.js @@ -2,7 +2,7 @@ import { options, Fragment, Component } from 'preact'; export function initDevTools() { if (typeof window != 'undefined' && window.__PREACT_DEVTOOLS__) { - window.__PREACT_DEVTOOLS__.attachPreact('10.20.0', options, { + window.__PREACT_DEVTOOLS__.attachPreact('10.20.1', options, { Fragment, Component }); diff --git a/package-lock.json b/package-lock.json index 29e49d78f0..684998501b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "preact", - "version": "10.20.0", + "version": "10.20.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "preact", - "version": "10.20.0", + "version": "10.20.1", "license": "MIT", "devDependencies": { "@actions/github": "^5.0.0", diff --git a/package.json b/package.json index 46db7c191b..7577d2e7a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "preact", "amdName": "preact", - "version": "10.20.0", + "version": "10.20.1", "private": false, "description": "Fast 3kb React-compatible Virtual DOM library.", "main": "dist/preact.js", From b1d1b8710368c949139d24a45017ca1de19a6df1 Mon Sep 17 00:00:00 2001 From: jviide Date: Sat, 23 Mar 2024 18:05:17 +0200 Subject: [PATCH 13/15] fix: use a virtual clock instead of Date.now() for event dispatch times (#4322) Fixes issue #4161. --- src/diff/props.js | 48 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/diff/props.js b/src/diff/props.js index 142e1444c9..45759bf6c1 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -13,6 +13,23 @@ function setStyle(style, key, value) { } } +// A "virtual clock" to solve issues like https://github.com/preactjs/preact/issues/3927. +// When the DOM performs an event it leaves micro-ticks in between bubbling up which means that +// an event can trigger on a newly reated DOM-node while the event bubbles up. +// +// Originally inspired by Vue https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/modules/events.ts#L90-L101, +// but modified to use a virtual clock instead of Date.now() in case event handlers get attached and +// events get dispatched during the same millisecond. +// +// Odd values are reserved for event dispatch times, and even values are reserved for new +// event handler attachment times. +// +// The clock is incremented before a new event is dispatched if the value is even +// (i.e a new event handler was attached after the previous new event). +// The clock is also incremented when a new event handler gets attached if the value is odd +// (i.e. a new event was dispatched after the previous new event dispatch). +let eventClock = 0; + /** * Set a property value on a DOM node * @param {PreactElement} dom The DOM node to modify @@ -68,7 +85,16 @@ export function setProperty(dom, name, value, oldValue, isSvg) { if (value) { if (!oldValue) { - value._attached = Date.now(); + // If any new events were dispatched between this moment and the last time + // an event handler was attached (i.e. `eventClock` is an odd number), + // then increment `eventClock` first. + // + // The following line is a compacted version of: + // if (eventClock % 2 === 1) { + // eventClock += 1; + // } + // value._attached = eventClock; + value._attached = eventClock += eventClock % 2; const handler = useCapture ? eventProxyCapture : eventProxy; dom.addEventListener(name, handler, useCapture); } else { @@ -131,18 +157,20 @@ export function setProperty(dom, name, value, oldValue, isSvg) { function eventProxy(e) { if (this._listeners) { const eventHandler = this._listeners[e.type + false]; - /** - * This trick is inspired by Vue https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/modules/events.ts#L90-L101 - * when the dom performs an event it leaves micro-ticks in between bubbling up which means that an event can trigger on a newly - * created DOM-node while the event bubbles up, this can cause quirky behavior as seen in https://github.com/preactjs/preact/issues/3927 - */ + // If e._dispatched is set, it has to be an odd number, so !e._dispatched must be true if set. if (!e._dispatched) { - // When an event has no _dispatched we know this is the first event-target in the chain - // so we set the initial dispatched time. - e._dispatched = Date.now(); + // If any new event handlers were attached after the previous new event dispatch + // (i.e. `eventClock` is an even number), then increment `eventClock` first. + // + // The following line is a compacted version of: + // if (eventClock % 2 === 0) { + // eventClock += 1; + // } + // e._dispatched = eventClock; + e._dispatched = eventClock += (eventClock + 1) % 2; // When the _dispatched is smaller than the time when the targetted event handler was attached // we know we have bubbled up to an element that was added during patching the dom. - } else if (e._dispatched <= eventHandler._attached) { + } else if (e._dispatched < eventHandler._attached) { return; } return eventHandler(options.event ? options.event(e) : e); From 8759dad5226e8a1e555c8b7cfdbb259f7d04cb2f Mon Sep 17 00:00:00 2001 From: jviide Date: Sun, 24 Mar 2024 08:55:18 +0200 Subject: [PATCH 14/15] refactor: simplify the logic introduced in #4322 (#4324) --- src/diff/props.js | 86 ++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 53 deletions(-) diff --git a/src/diff/props.js b/src/diff/props.js index 45759bf6c1..10961f18ea 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -13,21 +13,17 @@ function setStyle(style, key, value) { } } -// A "virtual clock" to solve issues like https://github.com/preactjs/preact/issues/3927. +// A logical clock to solve issues like https://github.com/preactjs/preact/issues/3927. // When the DOM performs an event it leaves micro-ticks in between bubbling up which means that // an event can trigger on a newly reated DOM-node while the event bubbles up. // -// Originally inspired by Vue https://github.com/vuejs/core/blob/main/packages/runtime-dom/src/modules/events.ts#L90-L101, -// but modified to use a virtual clock instead of Date.now() in case event handlers get attached and -// events get dispatched during the same millisecond. +// Originally inspired by Vue +// (https://github.com/vuejs/core/blob/caeb8a68811a1b0f79/packages/runtime-dom/src/modules/events.ts#L90-L101), +// but modified to use a logical clock instead of Date.now() in case event handlers get attached +// and events get dispatched during the same millisecond. // -// Odd values are reserved for event dispatch times, and even values are reserved for new -// event handler attachment times. -// -// The clock is incremented before a new event is dispatched if the value is even -// (i.e a new event handler was attached after the previous new event). -// The clock is also incremented when a new event handler gets attached if the value is odd -// (i.e. a new event was dispatched after the previous new event dispatch). +// The clock is incremented after each new event dispatch. This allows 1 000 000 new events +// per second for over 280 years before the value reaches Number.MAX_SAFE_INTEGER (2**53 - 1). let eventClock = 0; /** @@ -85,16 +81,8 @@ export function setProperty(dom, name, value, oldValue, isSvg) { if (value) { if (!oldValue) { - // If any new events were dispatched between this moment and the last time - // an event handler was attached (i.e. `eventClock` is an odd number), - // then increment `eventClock` first. - // - // The following line is a compacted version of: - // if (eventClock % 2 === 1) { - // eventClock += 1; - // } - // value._attached = eventClock; - value._attached = eventClock += eventClock % 2; + value._attached = eventClock; + const handler = useCapture ? eventProxyCapture : eventProxy; dom.addEventListener(name, handler, useCapture); } else { @@ -150,40 +138,32 @@ export function setProperty(dom, name, value, oldValue, isSvg) { } /** - * Proxy an event to hooked event handlers - * @param {PreactEvent} e The event object from the browser + * Create an event proxy function. + * @param {boolean} useCapture Is the event handler for the capture phase. * @private */ -function eventProxy(e) { - if (this._listeners) { - const eventHandler = this._listeners[e.type + false]; - // If e._dispatched is set, it has to be an odd number, so !e._dispatched must be true if set. - if (!e._dispatched) { - // If any new event handlers were attached after the previous new event dispatch - // (i.e. `eventClock` is an even number), then increment `eventClock` first. - // - // The following line is a compacted version of: - // if (eventClock % 2 === 0) { - // eventClock += 1; - // } - // e._dispatched = eventClock; - e._dispatched = eventClock += (eventClock + 1) % 2; - // When the _dispatched is smaller than the time when the targetted event handler was attached - // we know we have bubbled up to an element that was added during patching the dom. - } else if (e._dispatched < eventHandler._attached) { - return; +function createEventProxy(useCapture) { + /** + * Proxy an event to hooked event handlers + * @param {PreactEvent} e The event object from the browser + * @private + */ + return function (e) { + if (this._listeners) { + const eventHandler = this._listeners[e.type + useCapture]; + if (e._dispatched == null) { + e._dispatched = eventClock++; + + // When `e._dispatched` is smaller than the time when the targeted event + // handler was attached we know we have bubbled up to an element that was added + // during patching the DOM. + } else if (e._dispatched < eventHandler._attached) { + return; + } + return eventHandler(options.event ? options.event(e) : e); } - return eventHandler(options.event ? options.event(e) : e); - } + }; } -/** - * Proxy an event to hooked event handlers - * @param {PreactEvent} e The event object from the browser - * @private - */ -function eventProxyCapture(e) { - if (this._listeners) { - return this._listeners[e.type + true](options.event ? options.event(e) : e); - } -} +const eventProxy = createEventProxy(false); +const eventProxyCapture = createEventProxy(true); From d3d57db6ece98c5a3bbaa91777a4942f155935f6 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Mon, 25 Mar 2024 07:42:10 +0100 Subject: [PATCH 15/15] Some byte improvements (#4321) * save bytes during event registration * test performance improvement (does increase bytes) * optimize undefined setting * minor saving * undo perf to see byte impact --- src/component.js | 7 +++---- src/diff/index.js | 5 ++--- src/diff/props.js | 35 ++++++++++++++++++++--------------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/component.js b/src/component.js index 62b2439f72..4f4e5b2744 100644 --- a/src/component.js +++ b/src/component.js @@ -123,21 +123,20 @@ export function getDomSibling(vnode, childIndex) { function renderComponent(component) { let oldVNode = component._vnode, oldDom = oldVNode._dom, - parentDom = component._parentDom, commitQueue = [], refQueue = []; - if (parentDom) { + if (component._parentDom) { const newVNode = assign({}, oldVNode); newVNode._original = oldVNode._original + 1; if (options.vnode) options.vnode(newVNode); diff( - parentDom, + component._parentDom, newVNode, oldVNode, component._globalContext, - parentDom.ownerSVGElement !== undefined, + component._parentDom.ownerSVGElement !== undefined, oldVNode._flags & MODE_HYDRATE ? [oldDom] : null, commitQueue, oldDom == null ? getDomSibling(oldVNode) : oldDom, diff --git a/src/diff/index.js b/src/diff/index.js index b5bbb49db6..6289a9bd4d 100644 --- a/src/diff/index.js +++ b/src/diff/index.js @@ -579,7 +579,6 @@ export function unmount(vnode, parentVNode, skipRemove) { } r.base = r._parentDom = null; - vnode._component = undefined; } if ((r = vnode._children)) { @@ -588,7 +587,7 @@ export function unmount(vnode, parentVNode, skipRemove) { unmount( r[i], parentVNode, - skipRemove || typeof vnode.type !== 'function' + skipRemove || typeof vnode.type != 'function' ); } } @@ -600,7 +599,7 @@ export function unmount(vnode, parentVNode, skipRemove) { // Must be set to `undefined` to properly clean up `_nextDom` // for which `null` is a valid value. See comment in `create-element.js` - vnode._parent = vnode._dom = vnode._nextDom = undefined; + vnode._component = vnode._parent = vnode._dom = vnode._nextDom = undefined; } /** The `.render()` method for a PFC backing instance. */ diff --git a/src/diff/props.js b/src/diff/props.js index 10961f18ea..722f1a5061 100644 --- a/src/diff/props.js +++ b/src/diff/props.js @@ -82,15 +82,20 @@ export function setProperty(dom, name, value, oldValue, isSvg) { if (value) { if (!oldValue) { value._attached = eventClock; - - const handler = useCapture ? eventProxyCapture : eventProxy; - dom.addEventListener(name, handler, useCapture); + dom.addEventListener( + name, + useCapture ? eventProxyCapture : eventProxy, + useCapture + ); } else { value._attached = oldValue._attached; } } else { - const handler = useCapture ? eventProxyCapture : eventProxy; - dom.removeEventListener(name, handler, useCapture); + dom.removeEventListener( + name, + useCapture ? eventProxyCapture : eventProxy, + useCapture + ); } } else { if (isSvg) { @@ -99,18 +104,18 @@ export function setProperty(dom, name, value, oldValue, isSvg) { // - className --> class name = name.replace(/xlink(H|:h)/, 'h').replace(/sName$/, 's'); } else if ( - name !== 'width' && - name !== 'height' && - name !== 'href' && - name !== 'list' && - name !== 'form' && + name != 'width' && + name != 'height' && + name != 'href' && + name != 'list' && + name != 'form' && // Default value in browsers is `-1` and an empty string is // cast to `0` instead - name !== 'tabIndex' && - name !== 'download' && - name !== 'rowSpan' && - name !== 'colSpan' && - name !== 'role' && + name != 'tabIndex' && + name != 'download' && + name != 'rowSpan' && + name != 'colSpan' && + name != 'role' && name in dom ) { try {