diff --git a/src/diff/children.js b/src/diff/children.js index fd19a5555f..383b95cfdb 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -205,6 +205,17 @@ export function diffChildren( // Remove remaining oldChildren if there are any. for (i = oldChildrenLength; i--; ) { if (oldChildren[i] != null) { + if ( + typeof newParentVNode.type == 'function' && + oldChildren[i]._dom != null && + oldChildren[i]._dom == newParentVNode._nextDom + ) { + // If the newParentVNode.__nextDom points to a dom node that is about to + // be unmounted, then get the next sibling of that vnode and set + // _nextDom to it + newParentVNode._nextDom = newParentVNode._nextDom.nextSibling; + } + unmount(oldChildren[i], oldChildren[i]); } } diff --git a/test/browser/focus.test.js b/test/browser/focus.test.js index 004a87a1af..96348f6a3d 100644 --- a/test/browser/focus.test.js +++ b/test/browser/focus.test.js @@ -13,6 +13,9 @@ describe('focus', () => { /** @type {() => void} */ let rerender; + /** @type {(newState: Partial<{ before: number[]; after: number[] }>) => void} */ + let setState; + /** @type {() => void} */ let prepend, append, shift, pop; @@ -27,6 +30,8 @@ describe('focus', () => { after: props.initialAfter || [] }; + setState = newState => this.setState(newState); + prepend = () => { const before = this.state.before; const newValue = before[0] ? before[0] - 1 : 1; @@ -56,13 +61,14 @@ describe('focus', () => { }); }; - const liHtml = this.props.as == Input ? inputStr : span; - getDynamicListHtml = () => - div([ + getDynamicListHtml = () => { + const liHtml = this.props.as == Input ? inputStr : span; + return div([ ...this.state.before.map(liHtml), '', ...this.state.after.map(liHtml) ]); + }; } render(props, state) { @@ -377,6 +383,22 @@ describe('focus', () => { validateFocus(input, 'remove sibling before 2'); }); + it('should maintain focus when removing element directly before input', () => { + render( + , + scratch + ); + + let input = focusInput(); + expect(scratch.innerHTML).to.equal(getDynamicListHtml()); + + setState({ before: [0] }); + rerender(); + + expect(scratch.innerHTML).to.equal(getDynamicListHtml()); + validateFocus(input, 'remove sibling directly before input'); + }); + it('should maintain focus when adding input next to the current input', () => { render(, scratch); diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js index 937dd1d0d0..2dbbee6aab 100644 --- a/test/browser/fragments.test.js +++ b/test/browser/fragments.test.js @@ -237,9 +237,7 @@ describe('Fragment', () => { expectDomLogToBe([ '
.appendChild(#text)', '
122.insertBefore(
1, 1)', - '1.remove()', - '
122.appendChild(2)', - '
122.appendChild(2)' + '1.remove()' ]); }); @@ -2658,7 +2656,7 @@ describe('Fragment', () => { render(, scratch); expect(scratch.innerHTML).to.equal(div([div(1), div('A')])); - expectDomLogToBe(['
2.remove()', '
1A.appendChild(
A)']); + expectDomLogToBe(['
2.remove()']); }); it('should efficiently unmount nested Fragment children', () => { @@ -2857,13 +2855,12 @@ describe('Fragment', () => { '
123AB.insertBefore(1,
1)', '
2.remove()', '
3.remove()', - '
1.remove()', - '
1AB.appendChild(
A)', - '
1BA.appendChild(
B)' + '
1.remove()' ]); }); it('should swap nested fragments correctly', () => { + /** @type {() => void} */ let swap; class App extends Component { constructor(props) { @@ -2875,11 +2872,9 @@ describe('Fragment', () => { if (this.state.first) { return ( - { - -

1. Original item first paragraph

-
- } + +

1. Original item first paragraph

+

2. Original item second paragraph

' ); }); + + it('should efficiently unmount nested Fragment children when rerendering and reordering', () => { + /** @type {() => void} */ + let toggle; + + class App extends Component { + constructor(props) { + super(props); + this.state = { condition: true }; + toggle = () => this.setState({ condition: !this.state.condition }); + } + + render() { + return this.state.condition ? ( + +
1
+ +
A
+
B
+
+
2
+
+ ) : ( + + +
A
+
+
1
+
2
+
+ ); + } + } + + clearLog(); + render(, scratch); + expect(scratch.innerHTML).to.equal( + [div(1), div('A'), div('B'), div(2)].join('') + ); + + clearLog(); + toggle(); + rerender(); + + expect(scratch.innerHTML).to.equal([div('A'), div(1), div(2)].join('')); + expectDomLogToBe([ + '
B.remove()', + '
1A2.insertBefore(
1,
2)' + ]); + }); });