Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(quirksmode): use document.scrollingElement if possible #235

Merged
merged 17 commits into from May 13, 2018
Merged
4 changes: 3 additions & 1 deletion .gitignore
Expand Up @@ -3,6 +3,7 @@ node_modules
# Cypress stuff
**/cypress/screenshots
**/cypress/videos
**/junit

# Typescript stuff
typings
Expand All @@ -14,4 +15,5 @@ dist
es
umd
/compute.js
/index.js
/index.js
/types.js
1 change: 1 addition & 0 deletions .prettierignore
@@ -1,2 +1,3 @@
node_modules
tests/web-platform/resources
package.json
2 changes: 1 addition & 1 deletion src/compute.ts
Expand Up @@ -214,7 +214,7 @@ export default (
}

let targetRect = target.getBoundingClientRect()
const viewport = document.documentElement
const viewport = document.scrollingElement || document.documentElement

// Collect all the scrolling boxes, as defined in the spec: https://drafts.csswg.org/cssom-view/#scrolling-box
const frames: Element[] = []
Expand Down
13 changes: 9 additions & 4 deletions src/index.ts
@@ -1,9 +1,9 @@
import compute from './compute'
import {
ScrollBehavior,
CustomScrollBehaviorCallback,
CustomScrollAction,
CustomScrollBehaviorCallback,
Options as BaseOptions,
ScrollBehavior,
} from './types'

export interface StandardBehaviorOptions extends BaseOptions {
Expand All @@ -20,6 +20,7 @@ export interface Options<T = any> extends BaseOptions {
// Wait with checking if native smooth-scrolling exists until scrolling is invoked
// This is much more friendly to server side rendering envs, and testing envs like jest
let supportsScrollBehavior
let scrollingElement

const isFunction = (arg: any): arg is Function => {
return typeof arg == 'function'
Expand All @@ -32,8 +33,12 @@ const defaultBehavior = (
actions: CustomScrollAction[],
behavior: ScrollBehavior = 'auto'
) => {
// Because of quirksmode
if (!scrollingElement) {
scrollingElement = document.scrollingElement || document.documentElement
}
if (supportsScrollBehavior === undefined) {
supportsScrollBehavior = 'scrollBehavior' in document.documentElement.style
supportsScrollBehavior = 'scrollBehavior' in scrollingElement.style
}

actions.forEach(({ el, top, left }) => {
Expand All @@ -42,7 +47,7 @@ const defaultBehavior = (
if (el.scroll && supportsScrollBehavior) {
el.scroll({ top, left, behavior })
} else {
if (el === document.documentElement) {
if (el === scrollingElement) {
window.scrollTo(left, top)
} else {
el.scrollTop = top
Expand Down
22 changes: 22 additions & 0 deletions tests/web-platform/README.md
@@ -0,0 +1,22 @@
# Run tests

You'll need two terminal sessions for this.

1. Install deps `yarn`
2. Start the server: `yarn start`
3. In the other session: `yarn test`

You can also open http://localhost:3000 in the browser to run the tests.

# Why are tests setup like this?

Two reasons.

1. It helps with staying in sync with the same tests that browsers run when checking if they are implementing the spec correctly.
2. Easier to run tests in different browsers, compared to if a more conventional cypress or jest setup were used.

## How tests are kept in sync with w3c

The `resources` and `css` folders are rougly equivalent with their counterparts on here: https://github.com/w3c/web-platform-tests

Except that anything that isn't needed to run the tests related to `scrollIntoView` is stripped out.
102 changes: 102 additions & 0 deletions tests/web-platform/css/cssom-view/scrollIntoView-smooth.html
@@ -0,0 +1,102 @@
<!DOCTYPE HTML>
<script src="/node_modules/scroll-into-view-if-needed/umd/scroll-into-view-if-needed.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<title>Check End Position of smooth scrollIntoView</title>
<div id="container" style="height: 2500px; width: 2500px;">
<div id="content" style="height: 500px; width: 500px;margin-left: 1000px; margin-right: 1000px; margin-top: 1000px;margin-bottom: 1000px;background-color: red">
</div>
<div id="shadow"></div>
</div>
<script>
var content_height = 500;
var content_width = 500;
var window_height = document.scrollingElement.clientHeight;
var window_width = document.scrollingElement.clientWidth;
var content = document.getElementById("content");
add_completion_callback(() => document.getElementById("container").remove());

function waitForScrollEnd() {
var last_changed_frame = 0;
var last_x = window.scrollX;
var last_y = window.scrollY;
return new Promise((resolve, reject) => {
function tick(frames) {
// We requestAnimationFrame either for 500 frames or until 20 frames with
// no change have been observed.
if (frames >= 500 || frames - last_changed_frame > 20) {
resolve();
} else {
if (window.scrollX != last_x || window.scrollY != last_y) {
last_changed_frame = frames;
last_x = window.scrollX;
last_y = window.scrollY;
}
requestAnimationFrame(tick.bind(null, frames + 1));
}
}
tick(0);
});
}

// When testing manually, we need an additional frame at beginning
// to trigger the effect.
requestAnimationFrame(() => {
promise_test(t => {
window.scrollTo(0, 0);
var expected_x = content.offsetLeft + content_width - window_width;
var expected_y = content.offsetTop + content_height - window_height;
assert_not_equals(window.scrollX, expected_x, "scrollX");
assert_not_equals(window.scrollY, expected_y, "scrollY");
scrollIntoView(content, {behavior: "smooth", block: "nearest", inline:
"nearest"});
return waitForScrollEnd().then(() => {
assert_approx_equals(window.scrollX, expected_x, 1, "scrollX");
assert_approx_equals(window.scrollY, expected_y, 1, "scrollY");
});
}, "Smooth scrollIntoView should scroll the element to the 'nearest' position");

promise_test(t => {
window.scrollTo(0, 0);
var expected_x = content.offsetLeft;
var expected_y = content.offsetTop;
assert_not_equals(window.scrollX, expected_x, "scrollX");
assert_not_equals(window.scrollY, expected_y, "scrollY");
scrollIntoView(content, {behavior: "smooth", block: "start", inline:
"start"});
return waitForScrollEnd().then(() => {
assert_approx_equals(window.scrollX, expected_x, 1, "scrollX");
assert_approx_equals(window.scrollY, expected_y, 1, "scrollY");
});
}, "Smooth scrollIntoView should scroll the element to the 'start' position");

promise_test(t => {
window.scrollTo(0, 0);
var expected_x = content.offsetLeft + (content_width - window_width) / 2;
var expected_y = content.offsetTop + (content_height - window_height) / 2;
assert_not_equals(window.scrollX, expected_x, "scrollX");
assert_not_equals(window.scrollY, expected_y, "scrollY");
scrollIntoView(content, {behavior: "smooth", block: "center", inline:
"center"});
return waitForScrollEnd().then(() => {
assert_approx_equals(window.scrollX, expected_x, 1, "scrollX");
assert_approx_equals(window.scrollY, expected_y, 1, "scrollY");
});
}, "Smooth scrollIntoView should scroll the element to the 'center' position");

promise_test(t => {
window.scrollTo(0, 0);
var expected_x = content.offsetLeft + content_width - window_width;
var expected_y = content.offsetTop + content_height - window_height;
assert_not_equals(window.scrollX, expected_x, "scrollX");
assert_not_equals(window.scrollY, expected_y, "scrollY");
scrollIntoView(content, {behavior: "smooth", block: "end", inline:
"end"});
return waitForScrollEnd().then(() => {
assert_approx_equals(window.scrollX, expected_x, 1, "scrollX");
assert_approx_equals(window.scrollY, expected_y, 1, "scrollY");
});
}, "Smooth scrollIntoView should scroll the element to the 'end' position");

});
</script>
113 changes: 113 additions & 0 deletions tests/web-platform/css/cssom-view/scrollintoview.html
@@ -0,0 +1,113 @@
<!DOCTYPE html>
<title>CSSOM View - scrollIntoView</title>
<meta charset="utf-8">
<link rel="author" title="Chris Wu" href="mailto:pwx.frontend@gmail.com">
<link rel="help" href="https://drafts.csswg.org/cssom-view/#dom-element-scrollintoview">
<link rel="help" href="https://heycam.github.io/webidl/#es-operations">
<link rel="help" href="https://heycam.github.io/webidl/#es-overloads">
<meta name="flags" content="dom">
<script src="/node_modules/scroll-into-view-if-needed/umd/scroll-into-view-if-needed.js"></script>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
body.running { margin: 0; padding: 4000px; overflow: hidden }
body.running #testDiv {
width: 200px;
height: 200px;
background-color: lightgreen;
}
</style>
<body class=running>
<div id=testDiv></div>
<div id="log"></div>
<script>
var testDiv = document.getElementById('testDiv');

var expectedXLeft = 4000;
var expectedXRight = 4000 - window.innerWidth + testDiv.clientWidth;
var expectedXCenter = 4000 - (window.innerWidth / 2) + (testDiv.clientWidth / 2);

var expectedYTop = 4000;
var expectedYBottom = 4000 - window.innerHeight + testDiv.clientHeight;
var expectedYCenter = 4000 - (window.innerHeight / 2) + (testDiv.clientHeight / 2);

[
["omitted argument", "nearest", expectedYTop],
[true, "nearest", expectedYTop],
[false, "nearest", expectedYBottom],
[undefined, "nearest", expectedYTop],
[null, "nearest", expectedYTop],
[{}, "nearest", expectedYTop],
[{block: "center", inline: "center"}, expectedXCenter, expectedYCenter],
[{block: "start", inline: "start"}, expectedXLeft, expectedYTop],
[{block: "end", inline: "end"}, expectedXRight, expectedYBottom],
[{block: "nearest", inline: "nearest"}, "nearest", "nearest"],
].forEach(([input, expectedX, expectedY]) => {
test(() => {
window.scrollTo(0, 0);
testScrollIntoView(input);
var x = (expectedX === "nearest") ? expectedXRight : expectedX;
var y = (expectedY === "nearest") ? expectedYBottom : expectedY;
assert_approx_equals(window.scrollX, x, 0.5, 'scrollX');
assert_approx_equals(window.scrollY, y, 0.5, 'scrollY');
}, `scrollIntoView(${format_input(input)}) starting at left,top`);

test(() => {
window.scrollTo(0, 12000);
testScrollIntoView(input);
var x = (expectedX === "nearest") ? expectedXRight : expectedX;
var y = (expectedY === "nearest") ? expectedYTop : expectedY;
assert_approx_equals(window.scrollX, x, 0.5, 'scrollX');
assert_approx_equals(window.scrollY, y, 0.5, 'scrollY');
}, `scrollIntoView(${format_input(input)}) starting at left,bottom`);

test(() => {
window.scrollTo(12000, 0);
testScrollIntoView(input);
var x = (expectedX === "nearest") ? expectedXLeft : expectedX;
var y = (expectedY === "nearest") ? expectedYBottom : expectedY;
assert_approx_equals(window.scrollX, x, 0.5, 'scrollX');
assert_approx_equals(window.scrollY, y, 0.5, 'scrollY');
}, `scrollIntoView(${format_input(input)}) starting at right,top`);

test(() => {
window.scrollTo(12000, 12000);
testScrollIntoView(input);
var x = (expectedX === "nearest") ? expectedXLeft : expectedX;
var y = (expectedY === "nearest") ? expectedYTop : expectedY;
assert_approx_equals(window.scrollX, x, 0.5, 'scrollX');
assert_approx_equals(window.scrollY, y, 0.5, 'scrollY');
}, `scrollIntoView(${format_input(input)}) starting at right,bottom`);
});

function testScrollIntoView(input) {
if (input === "omitted argument") {
scrollIntoView(testDiv);
} else {
scrollIntoView(testDiv, input);
}
}

// This formats dict as a string suitable as test name.
// format_value() is provided by testharness.js,
// which also preserves sign for -0.
function format_dict(dict) {
const props = [];
for (let prop in dict) {
props.push(`${prop}: ${format_value(dict[prop])}`);
}
return `{${props.join(', ')}}`;
}

function format_input(input) {
if (input === "omitted argument") {
return "";
} else if (input === null || typeof input !== "object") {
return format_value(input);
}
return format_dict(input);
}

document.body.classList.remove('running');
window.scrollTo(0, 0);
</script>
18 changes: 9 additions & 9 deletions tests/web-platform/cypress/integration/index.js
@@ -1,13 +1,13 @@
describe('web-platform-tests by w3c', function() {
it('css/cssom-view/scrollintoview.html', () => {
cy.visit('/')
describe('css/cssom-view', () => {
// @TODO maybe there is a dynamic way to generate a list over tests

cy.get('#summary .pass').should('contain', '40 Pass')
})

it('scrollIntoView-smooth.html', () => {
cy.visit('/smooth')
const tests = ['scrollintoview.html', 'scrollIntoView-smooth.html']
tests.forEach(test => {
it(`implements ${test} correctly`, () => {
cy.visit(`/css/cssom-view/${test}`)

cy.get('#summary .pass').should('contain', '4 Pass')
cy.get('#summary .pass').should('exist')
cy.get('#summary .fail').should('not.exist')
})
})
})