| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>CSSOM View - scrollIntoView from position:fixed</title> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width,minimum-scale=1"> |
| <link rel="author" title="David Bokan" href="mailto:bokan@chromium.org"> |
| <link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-element-scrollintoview"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <style> |
| body { |
| width: 1000vw; |
| height: 1000vh; |
| /* stripes so we can see scroll offset more easily */ |
| background: repeating-linear-gradient( |
| 45deg, |
| #A2CFD9, |
| #A2CFD9 100px, |
| #C3F3FF 100px, |
| #C3F3FF 200px |
| ); |
| } |
| |
| .fixedContainer { |
| position: fixed; |
| bottom: 10px; |
| left: 10px; |
| width: 150px; |
| height: 150px; |
| background-color: coral; |
| } |
| |
| .fixedContainer.scrollable { |
| overflow: auto; |
| left: unset; |
| right: 10px; |
| } |
| |
| button { |
| position: absolute; |
| margin: 5px; |
| } |
| |
| .target { |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| background-color: blue; |
| left: 50%; |
| top: 50%; |
| } |
| |
| .scrollable .target { |
| left: 200%; |
| top: 200%; |
| } |
| |
| iframe { |
| width: 96vw; |
| height: 300px; |
| position: absolute; |
| left: 2vw; |
| top: 100px; |
| } |
| </style> |
| <script> |
| </script> |
| </head> |
| <body> |
| <div style="width:90vw"> |
| <p> |
| The orange boxes are position: fixed. Clicking ScrollIntoView in each box |
| will attempt to scroll into view the blue target element inside that fixed |
| container to block/inline: start (i.e. aligned with top left corner in RTL). |
| </p> |
| <p> |
| scrollIntoView from a position:fixed element must not scroll its |
| containing frame; however, it must scroll further ancestor scrollers as |
| the element isn't fixed in relation to them. |
| </p> |
| </div> |
| <iframe></iframe> |
| <div class="fixedContainer"> |
| Box A |
| <button id="fixedContainerBtn">ScrollIntoView</button> |
| <div class="target"></div> |
| </div> |
| <div class="fixedContainer scrollable"> |
| Box C |
| <button id="scrollableFixedContainerBtn">ScrollIntoView</button> |
| <div class="target"></div> |
| </div> |
| <script> |
| if (typeof setup != 'undefined') { |
| setup({ explicit_done: true }); |
| window.addEventListener("load", runTests); |
| } |
| |
| function scrollIntoView(evt) { |
| const container = evt.target.closest('.fixedContainer'); |
| const target = container.querySelector('.target'); |
| target.scrollIntoView({block: 'start', inline: 'start'}); |
| } |
| |
| document.querySelectorAll('button').forEach((btn) => { |
| btn.addEventListener('click', scrollIntoView); |
| }); |
| |
| const iframe = document.querySelector('iframe'); |
| iframe.onload = () => { |
| frames[0].document.querySelectorAll('button').forEach((btn) => { |
| btn.addEventListener('click', scrollIntoView); |
| }); |
| } |
| iframe.srcdoc = ` |
| <!DOCTYPE html> |
| <style> |
| body { |
| height: 200vh; |
| width: 200vw; |
| /* stripes so we can see scroll offset more easily */ |
| background: repeating-linear-gradient( |
| 45deg, |
| #C3A2D9, |
| #C3A2D9 50px, |
| #E5C0FF 50px, |
| #E5C0FF 100px |
| ); |
| } |
| .fixedContainer { |
| position: fixed; |
| bottom: 10px; |
| left: 10px; |
| width: 150px; |
| height: 150px; |
| background-color: coral; |
| } |
| |
| .fixedContainer.scrollable { |
| overflow: auto; |
| left: unset; |
| right: 10px; |
| } |
| |
| button { |
| position: absolute; |
| margin: 5px; |
| } |
| |
| .target { |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| background-color: blue; |
| left: 50%; |
| top: 50%; |
| } |
| |
| .scrollable .target { |
| left: 200%; |
| top: 200%; |
| } |
| </style> |
| IFRAME |
| <div class="fixedContainer"> |
| Box B |
| <button id="fixedContainerBtn">ScrollIntoView</button> |
| <div class="target"></div> |
| </div> |
| <div class="fixedContainer scrollable"> |
| Box D |
| <button id="scrollableFixedContainerBtn">ScrollIntoView</button> |
| <div class="target"></div> |
| </div> |
| `; |
| |
| function reset() { |
| [document, frames[0].document].forEach((doc) => { |
| doc.scrollingElement.scrollLeft = 0; |
| doc.scrollingElement.scrollTop = 0; |
| doc.querySelectorAll('.fixedContainer').forEach((e) => { |
| e.scrollLeft = 0; |
| e.scrollTop = 0; |
| }); |
| }); |
| } |
| |
| function runTests() { |
| // Test scrollIntoView from a plain, unscrollable position:fixed element. |
| // Nothing should scroll. |
| test(() => { |
| reset() |
| const container = document.querySelector('.fixedContainer:not(.scrollable)'); |
| const target = container.querySelector('.target'); |
| target.scrollIntoView({block: 'start', inline: 'start'}); |
| assert_equals(window.scrollX, 0, 'must not scroll window [scrollX]'); |
| assert_equals(window.scrollY, 0, 'must not scroll window [scrollY]'); |
| }, `[Box A] scrollIntoView from unscrollable position:fixed`); |
| |
| // Same as above but from inside an iframe. Since the container is fixed |
| // only to the iframe, we should scroll the outer window. |
| test(() => { |
| reset() |
| const container = frames[0].document.querySelector('.fixedContainer:not(.scrollable)'); |
| const target = container.querySelector('.target'); |
| target.scrollIntoView({block: 'start', inline: 'start'}); |
| |
| // Large approx to account for differences like scrollbars |
| assert_approx_equals(window.scrollX, 100, 20, 'must scroll outer window [scrollX]'); |
| assert_approx_equals(window.scrollY, 300, 20, 'must scroll outer window [scrollY]'); |
| assert_equals(frames[0].window.scrollX, 0, 'must not scroll iframe [scrollX]'); |
| assert_equals(frames[0].window.scrollY, 0, 'must not scroll iframe [scrollY]'); |
| }, `[Box B] scrollIntoView from unscrollable position:fixed in iframe`); |
| |
| // Test scrollIntoView from a scroller that's position fixed. The |
| // scroller should be scrolled but shouldn't bubble the scroll as the |
| // window scrolling won't change the target's position. |
| test(() => { |
| reset() |
| const container = document.querySelector('.fixedContainer.scrollable'); |
| const target = container.querySelector('.target'); |
| target.scrollIntoView({block: 'start', inline: 'start'}); |
| // Large approx to account for differences like scrollbars |
| assert_equals(window.scrollX, 0, 'must not scroll window [scrollX]'); |
| assert_equals(window.scrollY, 0, 'must not scroll window [scrollY]'); |
| assert_approx_equals(container.scrollLeft, 145, 20, |
| 'scrollIntoView in container [scrollLeft]'); |
| assert_approx_equals(container.scrollTop, 145, 20, |
| 'scrollIntoView in container [scrollTop]'); |
| }, `[Box C] scrollIntoView from scrollable position:fixed`); |
| |
| // Same as above but from inside an iframe. In this case, the scroller |
| // should bubble the scroll but skip its own frame (as it's fixed with |
| // respect to its own frame's scroll offset) and bubble to the outer |
| // window. |
| test(() => { |
| reset() |
| const container = frames[0].document.querySelector('.fixedContainer.scrollable'); |
| const target = container.querySelector('.target'); |
| target.scrollIntoView({block: 'start', inline: 'start'}); |
| // Large approx to account for differences like scrollbars |
| assert_approx_equals(window.scrollX, 740, 20, 'must scroll outer window [scrollX]'); |
| assert_approx_equals(window.scrollY, 360, 20, 'must scroll outer window [scrollY]'); |
| assert_approx_equals(container.scrollLeft, 145, 20, |
| 'scrollIntoView in container [scrollLeft]'); |
| assert_approx_equals(container.scrollTop, 145, 20, |
| 'scrollIntoView in container [scrollTop]'); |
| assert_equals(frames[0].window.scrollX, 0, 'must not scroll iframe [scrollX]'); |
| assert_equals(frames[0].window.scrollY, 0, 'must not scroll iframe [scrollY]'); |
| }, `[Box D] scrollIntoView from scrollable position:fixed in iframe`); |
| |
| done(); |
| } |
| |
| </script> |
| </body> |
| </html> |