Skip to content

Commit

Permalink
feat: Add option restoreScroll
Browse files Browse the repository at this point in the history
  • Loading branch information
WilcoFiers committed Jun 19, 2017
1 parent 89b8bab commit d55f3cd
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 9 deletions.
1 change: 1 addition & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ Additionally, there are a number or properties that allow configuration of diffe
| `absolutePaths` | `false` | Use absolute paths when creating element selectors
| `iframes` | `true` | Tell axe to run inside iframes
| `elementRef` | `false` | Return element references in addition to the target
| `restoreScroll` | `false` | Scrolls elements back to before axe started
###### Options Parameter Examples
Expand Down
9 changes: 8 additions & 1 deletion lib/core/public/run-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ function runRules(context, options, resolve, reject) {
});
}
q.defer(function (res, rej) {
audit.run(context, options, res, rej);
if (options.restoreScroll) {
const scrollState = axe.utils.getScrollState();
audit.run(context, options, res, rej);
axe.utils.setScrollState(scrollState);

} else {
audit.run(context, options, res, rej);
}
});
q.then(function (data) {
try {
Expand Down
69 changes: 69 additions & 0 deletions lib/core/utils/scroll-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Return the scroll position of scrollable elements
*/
function getScroll (elm) {
const style = window.getComputedStyle(elm);
const visibleOverflowY = style.getPropertyValue('overflow-y') === 'visible';
const visibleOverflowX = style.getPropertyValue('overflow-x') === 'visible';

if (// See if the element hides overflowing content
(!visibleOverflowY && elm.scrollHeight > elm.clientHeight) ||
(!visibleOverflowX && elm.scrollWidth > elm.clientWidth)
) {
return { elm, top: elm.scrollTop, left: elm.scrollLeft };
}
}

/**
* set the scroll position of an element
*/
function setScroll (elm, top, left) {
if (elm === window) {
return elm.scroll(top, left);
} else {
elm.scrollTop = top;
elm.scrollLeft = left;
}
}

/**
* Create an array scroll positions from descending elements
*/
function getElmScrollRecursive (root) {
return Array.from(root.children).reduce((scrolls, elm) => {
const scroll = getScroll(elm);
if (scroll) {
scrolls.push(scroll);
}
return scrolls.concat(getElmScrollRecursive(elm));
}, []);
}

/**
* Get the scroll position of all scrollable elements in a page
*/
axe.utils.getScrollState = function getScrollState (win = window) {
const root = win.document.documentElement;
const windowScroll = [(win.pageXOffset !== undefined ? {
elm: win,
top: win.pageYOffset,
left: win.pageXOffset
} : {
elm: root,
top: root.scrollTop,
left: root.scrollLeft
})];

return windowScroll.concat(
getElmScrollRecursive(document.body)
);
};

/**
* set the scroll position of all items in the scrollState array
*/
axe.utils.setScrollState = function setScrollState (scrollState) {
scrollState.forEach(
({ elm, top, left }) => setScroll(elm, top, left)
);
};
43 changes: 43 additions & 0 deletions test/core/public/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,49 @@ describe('axe.run', function () {
});
});
});

describe('option restoreScroll', function () {
it('does not change scroll when restoreScroll is not set', function (done) {
var calls = 0;
var _getSS = axe.utils.getScrollState;
var _setSS = axe.utils.setScrollState;
axe.utils.setScrollState = function () {
calls++;
};
axe.utils.getScrollState = axe.utils.setScrollState;

axe.run('#fixture', {}, function () {
assert.equal(calls, 0);
axe.utils.getScrollState = _getSS;
axe.utils.setScrollState = _setSS;
done();
});
});

it('resets scrolLState after running the audit', function (done) {
var scrollState = {};
var calls = 0;
var _getSS = axe.utils.getScrollState;
var _setSS = axe.utils.setScrollState;

axe.utils.setScrollState = function (arg) {
assert.equal(scrollState, arg);
calls++;
};
axe.utils.getScrollState = function () {
return scrollState;
};

axe.run('#fixture', {
restoreScroll: true
}, function () {
assert.equal(calls, 1);
axe.utils.getScrollState = _getSS;
axe.utils.setScrollState = _setSS;
done();
});
});
});
});

describe('axe.run iframes', function () {
Expand Down
163 changes: 163 additions & 0 deletions test/core/utils/scroll-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
describe('axe.utils.getScrollState', function () {
'use strict';
var mockWin;
var getScrollState = axe.utils.getScrollState;

var fixture = document.getElementById('fixture');

beforeEach(function () {
mockWin = {
pageXOffset: 1,
pageYOffset: 3,
document: {
documentElement: {
children: [],
scrollTop: 3,
scrollHeight: 4
}
},
body: { children: [] }
};
fixture.innerHTML = '';
});

it('should be a function', function () {
assert.isFunction(getScrollState);
});

it('takes the window object as an optional argument', function () {
assert.deepEqual(
getScrollState(),
getScrollState(window)
);
});

it('returns the window as the first item, if pageXOffset is supported', function () {
assert.deepEqual(
getScrollState(mockWin)[0],
{
elm: mockWin,
top: mockWin.pageYOffset,
left: mockWin.pageXOffset
}
);
});

it('returns the html as the first item, if pageXOffset is not supported', function () {
mockWin.pageYOffset = undefined;
mockWin.pageXOffset = undefined;
var html = mockWin.document.documentElement;

assert.deepEqual(
getScrollState(mockWin)[0],
{
elm: html,
top: html.scrollTop,
left: html.scrollLeft
}
);
});

it('grabs scrollTop and scrollLeft from all descendants of body', function () {
fixture.innerHTML =
'<div style="overflow:auto; height: 50px" id="tgt1">' +
'<div style="height: 100px"> Han Solo </div>' +
'<div style="overflow: hidden; height: 50px" id="tgt2">' +
'<div style="height: 100px"> Chewbacca </div>' +
'</div>' +
'</div>';

var tgt1 = document.getElementById('tgt1');
var tgt2 = document.getElementById('tgt2');
tgt1.scrollTop = 10;
tgt2.scrollTop = 20;

var scrollState = getScrollState();

assert.deepEqual(
scrollState.find(function (scroll) { return scroll.elm === tgt1; }),
{ elm: tgt1, top: 10, left: 0 }
);
assert.deepEqual(
scrollState.find(function (scroll) { return scroll.elm === tgt2; }),
{ elm: tgt2, top: 20, left: 0 }
);
});

it('ignores elements with overflow visible', function () {
fixture.innerHTML =
'<div style="overflow:visible; height: 50px" id="tgt1">' +
'<div style="height: 100px" id="tgt2"> Han Solo </div>' +
'</div>';

var tgt1 = document.getElementById('tgt1');
var tgt2 = document.getElementById('tgt2');
var scrollState = getScrollState();

assert.isUndefined(
scrollState.find(function (scroll) { return scroll.elm === tgt1; })
);
assert.isUndefined(
scrollState.find(function (scroll) { return scroll.elm === tgt2; })
);
});

it('ignores elements that do not overflow', function () {
fixture.innerHTML =
'<div style="overflow:auto; height: 300px" id="tgt1">' +
'<div style="height: 100px"> Han Solo </div>' +
'<div style="overflow: hidden; height: 150px" id="tgt2">' +
'<div style="height: 100px"> Chewbacca </div>' +
'</div>' +
'</div>';

var tgt1 = document.getElementById('tgt1');
var tgt2 = document.getElementById('tgt2');
var scrollState = getScrollState();

assert.isUndefined(
scrollState.find(function (scroll) { return scroll.elm === tgt1; })
);
assert.isUndefined(
scrollState.find(function (scroll) { return scroll.elm === tgt2; })
);
});
});

describe('axe.utils.setScrollState', function () {
'use strict';
var setScrollState = axe.utils.setScrollState;

var fixture = document.getElementById('fixture');
afterEach(function () {
fixture.innerHTML = '';
});

it('should be a function', function () {
assert.isFunction(setScrollState);
});

it('sets scrollTop and scrollLeft for regular nodes', function () {
var elm1 = {}, elm2 = {};
setScrollState([
{ elm: elm1, top: 10, left: 20 },
{ elm: elm2, top: 30, left: 40 },
]);

assert.deepEqual(elm1, { scrollTop: 10, scrollLeft: 20 });
assert.deepEqual(elm2, { scrollTop: 30, scrollLeft: 40 });
});

it('calls scroll() for the window element', function () {
var called;
var winScroll = window.scroll;
window.scroll = function (top, left) {
called = { top: top, left: left };
};
setScrollState([
{ elm: window, top: 10, left: 20 }
]);
assert.deepEqual(called, { top: 10, left: 20 });
window.scroll = winScroll;
});
});
27 changes: 19 additions & 8 deletions test/playground.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
<!doctype html>
<html lang="en">
<title>O hai</title>

<div style="
height: 50px;
width: 50px;
border: solid 1px black;
overflow: auto;
">
<p>foofoofoofoofoofoo</p>
<p>foofoofoofoofoofoo</p>
<p>foofoofoofoofoofoo</p>
<p>foofoofoofoofoofoo</p>
<p>foofoofoofoofoofoo</p>
</div>

<script src="/axe.js"></script>
<div id="dupe"></div>
<div id="dupe"></div>
<script>

window.addEventListener('load' , function () {
axe.a11yCheck(document, {}, function (res) {
console.log(res);
});
axe.a11yCheck(document, { restoreScroll: true }, function (res) {
console.log(res);
});
})

</script>
</script>

0 comments on commit d55f3cd

Please sign in to comment.