diff --git a/bridge/core/binding_call_methods.json5 b/bridge/core/binding_call_methods.json5 index e4f8d8c6a7..e1cbe4bf59 100644 --- a/bridge/core/binding_call_methods.json5 +++ b/bridge/core/binding_call_methods.json5 @@ -179,6 +179,7 @@ "dir", "pageXOffset", "pageYOffset", - "title" + "title", + "__test_global_to_local__" ] } diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc index a54d131732..eff9839b7c 100644 --- a/bridge/core/dom/element.cc +++ b/bridge/core/dom/element.cc @@ -464,6 +464,18 @@ ScriptPromise Element::toBlob(double device_pixel_ratio, ExceptionState& excepti return resolver->Promise(); } +ScriptValue Element::___testGlobalToLocal__(double x, double y, webf::ExceptionState& exception_state) { + const NativeValue args[] = { + NativeValueConverter::ToNativeValue(x), + NativeValueConverter::ToNativeValue(y), + }; + + NativeValue result = InvokeBindingMethod(binding_call_methods::k__test_global_to_local__, 2, args, + FlushUICommandReason::kDependentsOnElement | FlushUICommandReason::kDependentsOnLayout, exception_state); + + return ScriptValue(ctx(), result); +} + void Element::DidAddAttribute(const AtomicString& name, const AtomicString& value) {} void Element::WillModifyAttribute(const AtomicString& name, diff --git a/bridge/core/dom/element.d.ts b/bridge/core/dom/element.d.ts index 03b6a218a4..18dd5dc03b 100644 --- a/bridge/core/dom/element.d.ts +++ b/bridge/core/dom/element.d.ts @@ -76,5 +76,7 @@ interface Element extends Node, ParentNode, ChildNode { // WebF special API. toBlob(devicePixelRatioValue?: double): Promise; + __testGlobalToLocal__(x: number, y: number): any; + new(): void; } diff --git a/bridge/core/dom/element.h b/bridge/core/dom/element.h index 74d5833957..0707eed235 100644 --- a/bridge/core/dom/element.h +++ b/bridge/core/dom/element.h @@ -81,6 +81,8 @@ class Element : public ContainerNode { ScriptPromise toBlob(double device_pixel_ratio, ExceptionState& exception_state); ScriptPromise toBlob(ExceptionState& exception_state); + ScriptValue ___testGlobalToLocal__(double x, double y, ExceptionState& exception_state); + void DidAddAttribute(const AtomicString&, const AtomicString&); void WillModifyAttribute(const AtomicString&, const AtomicString& old_value, const AtomicString& new_value); void DidModifyAttribute(const AtomicString&, diff --git a/integration_tests/specs/dom/nodes/element.ts b/integration_tests/specs/dom/nodes/element.ts index a91f6029fb..21d1b77e78 100644 --- a/integration_tests/specs/dom/nodes/element.ts +++ b/integration_tests/specs/dom/nodes/element.ts @@ -77,10 +77,96 @@ describe('DOM Element API', () => { }); + it('should work with scroll with fixed elements', async () => { + const style = document.createElement('style'); + style.innerHTML = `.container { + margin: 64px 0 32px; + text-align: center; + padding-top: 1000px; + padding-bottom: 300px; + background: linear-gradient(to right, #ff7e5f, #feb47b); + }`; + document.head.appendChild(style); + + const container = createElement('div', { + className: 'container', + }, [ + createElement('div', { + style: { + position: 'fixed', + top: '300px', + left: 0, + } + }, [ + createElement('div', { + id: 'box' + }, [ + createText('click me') + ]) + ]) + ]); + + BODY.append(container); + + const clickBox = document.querySelector('#box'); + + const rect1 = clickBox?.getBoundingClientRect(); + + window.scrollTo(0, 200); + + const rect2 = clickBox?.getBoundingClientRect(); + + expect(JSON.stringify(rect1)).toEqual(JSON.stringify(rect2)); + }); + + it('should works with globalToLocal transform with position fixed layout', async () => { + const style = document.createElement('style'); + style.innerHTML = `.container { + margin: 64px 0 32px; + text-align: center; + padding-top: 1000px; + padding-bottom: 300px; + background: linear-gradient(to right, #ff7e5f, #feb47b); + }`; + document.head.appendChild(style); + + const container = createElement('div', { + className: 'container', + }, [ + createElement('div', { + style: { + position: 'fixed', + top: '300px', + left: 0, + } + }, [ + createElement('div', { + id: 'box' + }, [ + createText('click me') + ]) + ]) + ]); + + BODY.append(container); + + const clickBox = document.querySelector('#box'); + const rect = clickBox?.getBoundingClientRect(); + + // @ts-ignore + const offset1 = clickBox?.___testGlobalToLocal__(rect.x, rect.y + 10); + + window.scrollTo(0, 200); + + // @ts-ignore + const offset2 = clickBox?.___testGlobalToLocal__(rect?.x, rect.y + 10); + expect(JSON.stringify(offset1)).toEqual(JSON.stringify(offset2)); + }); + it('should works when getting multiple zero rects', () => { const div = document.createElement('div'); - expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0}); - expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0}); + expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 }); + expect(JSON.parse(JSON.stringify(div.getBoundingClientRect()))).toEqual({ bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0, x: 0, y: 0 }); }); it('children should only contain elements', () => { @@ -120,7 +206,7 @@ describe('DOM Element API', () => { el.appendChild(document.createTextNode('text')); el.appendChild(document.createComment('comment')); - for (let i = 0; i < 20; i ++) { + for (let i = 0; i < 20; i++) { el.appendChild(document.createElement('span')); } @@ -132,7 +218,7 @@ describe('DOM Element API', () => { const el = document.createElement('div'); document.body.appendChild(el); - for (let i = 0; i < 20; i ++) { + for (let i = 0; i < 20; i++) { el.appendChild(document.createElement('span')); } el.appendChild(document.createTextNode('text')); @@ -174,16 +260,16 @@ describe('DOM Element API', () => { el.appendChild(document.createElement('span')); var target = el.lastElementChild?.parentElement; expect(target.tagName).toEqual('DIV'); - + let childDiv = document.createDocumentFragment().appendChild(document.createElement('div')); expect(childDiv.parentElement).toEqual(null); - + expect(document.documentElement.parentElement).toEqual(null); }); }); describe('children', () => { - test(function() { + test(function () { var container = document.createElement('div'); container.innerHTML = ''; var list = container.children; diff --git a/integration_tests/specs/dom/nodes/event-target.ts b/integration_tests/specs/dom/nodes/event-target.ts index 8b95df0baf..615aae7192 100644 --- a/integration_tests/specs/dom/nodes/event-target.ts +++ b/integration_tests/specs/dom/nodes/event-target.ts @@ -231,4 +231,45 @@ describe('DOM EventTarget', () => { }); + it('should work with scroll with fixed elements', async (done) => { + const style = document.createElement('style'); + style.innerHTML = `.container { + margin: 64px 0 32px; + text-align: center; + padding-top: 1000px; + padding-bottom: 300px; + background: linear-gradient(to right, #ff7e5f, #feb47b); + }`; + document.head.appendChild(style); + + const container = createElement('div', { + className: 'container', + }, [ + createElement('div', { + style: { + position: 'fixed', + top: '300px', + left: 0, + } + }, [ + createElement('div', { + id: 'box' + }, [ + createText('click me') + ]) + ]) + ]); + + BODY.append(container); + + window.scrollTo(0, 200); + + const clickBox = document.querySelector('#box'); + clickBox?.addEventListener('click', () => { + done(); + }); + + await simulateClick(10, 300); + }); + }); diff --git a/webf/lib/src/dom/element.dart b/webf/lib/src/dom/element.dart index c44fe714be..81b12ff8e3 100644 --- a/webf/lib/src/dom/element.dart +++ b/webf/lib/src/dom/element.dart @@ -330,6 +330,10 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin methods['querySelector'] = BindingObjectMethodSync(call: (args) => querySelector(args)); methods['matches'] = BindingObjectMethodSync(call: (args) => matches(args)); methods['closest'] = BindingObjectMethodSync(call: (args) => closest(args)); + + if (kDebugMode || kProfileMode) { + methods['__test_global_to_local__'] = BindingObjectMethodSync(call: (args) => testGlobalToLocal(args[0], args[1])); + } } dynamic getElementsByClassName(List args) { @@ -1821,6 +1825,16 @@ abstract class Element extends ContainerNode with ElementBase, ElementEventMixin return offset; } + dynamic testGlobalToLocal(double x, double y) { + if (!isRendererAttached) { + return { 'x': 0, 'y': 0 }; + } + + Offset offset = Offset(x, y); + Offset result = renderBoxModel!.globalToLocal(offset); + return {'x': result.dx, 'y': result.dy}; + } + // The HTMLElement.offsetTop read-only property returns the distance of the outer border // of the current element relative to the inner border of the top of the offsetParent node. // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsettop diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 264fbe791f..2360b38061 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -43,7 +43,7 @@ Offset getLayoutTransformTo(RenderObject current, RenderObject ancestor, {bool e assert(renderer.parent != null); } renderers.add(ancestor); - Offset offset = Offset.zero; + List stackOffsets = []; final Matrix4 transform = Matrix4.identity(); for (int index = renderers.length - 1; index > 0; index -= 1) { @@ -51,18 +51,26 @@ Offset getLayoutTransformTo(RenderObject current, RenderObject ancestor, {bool e RenderObject childRenderer = renderers[index - 1]; // Apply the layout transform for renderBoxModel and fallback to paint transform for other renderObject type. if (parentRenderer is RenderBoxModel) { - offset += parentRenderer.obtainLayoutTransform(childRenderer, excludeScrollOffset); + // If the next renderBox has a fixed position, + // the outside scroll offset won't affect the actual results because of its fixed positioning. + if (childRenderer is RenderBoxModel && childRenderer.renderStyle.position == CSSPositionType.fixed) { + stackOffsets.clear(); + } + + stackOffsets.add(parentRenderer.obtainLayoutTransform(childRenderer, excludeScrollOffset)); } else if (parentRenderer is RenderSliverRepaintProxy) { parentRenderer.applyLayoutTransform(childRenderer, transform, excludeScrollOffset); } else if (parentRenderer is RenderBox) { assert(childRenderer.parent == parentRenderer); if (childRenderer.parentData is BoxParentData) { - offset += (childRenderer.parentData as BoxParentData).offset; + stackOffsets.add((childRenderer.parentData as BoxParentData).offset); } } } - return offset; + if (stackOffsets.isEmpty) return Offset.zero; + + return stackOffsets.reduce((prev, next) => prev + next); } /// Modified from Flutter rendering/box.dart. diff --git a/webf/lib/src/rendering/overflow.dart b/webf/lib/src/rendering/overflow.dart index 6cf118ffc6..6ca3cb99c5 100644 --- a/webf/lib/src/rendering/overflow.dart +++ b/webf/lib/src/rendering/overflow.dart @@ -254,8 +254,20 @@ mixin RenderOverflowMixin on RenderBoxModelBase { return result; } + + // For position fixed render box, should reduce the outer scroll offsets. + void applyPositionFixedPaintTransform(RenderBoxModel child, Matrix4 transform) { + Offset totalScrollOffset = child.getTotalScrollOffset(); + transform.translate(totalScrollOffset.dx, totalScrollOffset.dy); + } + void applyOverflowPaintTransform(RenderBox child, Matrix4 transform) { final Offset paintOffset = Offset(_paintOffsetX, _paintOffsetY); + + if (child is RenderBoxModel && child.renderStyle.position == CSSPositionType.fixed) { + applyPositionFixedPaintTransform(child, transform); + } + transform.translate(paintOffset.dx, paintOffset.dy); }