Skip to content
This repository has been archived by the owner on Dec 19, 2024. It is now read-only.

Commit

Permalink
IronScrollManager and scrollAction (#234)
Browse files Browse the repository at this point in the history
* copy iron-dropdown-scroll-manager from iron-dropdown

* add allowOutsideScroll, add tests

* Copyright (c) 2017

* restore scroll position when scroll happens (because a11y)

* hope to fix ie10

* get scroll info from wheelDelta (IE10)

* minimal listeners

* listen to WebComponentsReady

* Update tests scrollEvents

* use wct 5, update demo

* remove elementIsScrollLocked, cleanup tests

* listen to all scroll-triggering events

* Listen scroll events on html only

* restore elementIsScrollLocked

* noOutsideScroll & refitOnScroll

* scrollAction

* incorporate feedback

* s/Delays/Debounces
  • Loading branch information
valdrinkoshi authored Oct 27, 2017
1 parent 7ce638b commit 6d28420
Show file tree
Hide file tree
Showing 7 changed files with 939 additions and 25 deletions.
6 changes: 3 additions & 3 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ <h3>Use <code>with-backdrop</code> to add a backdrop to your overlay. Tabbing wi
<demo-snippet>
<template>
<button onclick="backdrop.open()">Overlay with backdrop</button>
<simple-overlay id="backdrop" with-backdrop>
<simple-overlay id="backdrop" with-backdrop scroll-action="lock">
<p>Hello world!</p>
<button>Button</button>
<button onclick="backdrop.close()">Close</button>
Expand Down Expand Up @@ -140,7 +140,7 @@ <h3>An element with <code>IronOverlayBehavior</code> can be scrollable or contai
</custom-style>
<button onclick="scrolling.open()">Scrolling overlay</button>

<simple-overlay id="scrolling" class="with-margin scrollable" tabindex=-1>
<simple-overlay id="scrolling" class="with-margin scrollable" tabindex=-1 scroll-action="lock">
<h2>This overlay scrolls internally.</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Expand Down Expand Up @@ -205,7 +205,7 @@ <h2>This overlay scrolls internally.</h2>

<button onclick="scrolling2.open()">Scrolling content</button>

<simple-overlay id="scrolling2" class="with-margin" tabindex=-1>
<simple-overlay id="scrolling2" class="with-margin" tabindex=-1 scroll-action="lock">
<h2>This overlay has a scrolling child.</h2>
<p class="scrollable">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Expand Down
150 changes: 128 additions & 22 deletions iron-overlay-behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<link rel="import" href="../iron-fit-behavior/iron-fit-behavior.html">
<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="iron-overlay-manager.html">
<link rel="import" href="iron-scroll-manager.html">
<link rel="import" href="iron-focusables-helper.html">

<script>
Expand Down Expand Up @@ -112,6 +113,17 @@
type: Boolean
},

/**
* Determines which action to perform when scroll outside an opened overlay happens.
* Possible values:
* lock - blocks scrolling from happening,
* refit - computes the new position on the overlay
* cancel - causes the overlay to close
*/
scrollAction: {
type: String
},

/**
* Shortcut to access to the overlay manager.
* @private
Expand All @@ -136,6 +148,10 @@
'iron-resize': '_onIronResize'
},

observers: [
'__updateScrollObservers(isAttached, opened, scrollAction)'
],

/**
* The backdrop element.
* @type {Element}
Expand Down Expand Up @@ -174,10 +190,13 @@
this.__shouldRemoveTabIndex = false;
// Used for wrapping the focus on TAB / Shift+TAB.
this.__firstFocusableNode = this.__lastFocusableNode = null;
// Used by __onNextAnimationFrame to cancel any previous callback.
this.__raf = null;
// Used by to keep track of the RAF callbacks.
this.__rafs = {};
// Focused node before overlay gets opened. Can be restored on close.
this.__restoreFocusNode = null;
// Scroll info to be restored.
this.__scrollTop = this.__scrollLeft = null;
this.__onCaptureScroll = this.__onCaptureScroll.bind(this);
this._ensureSetup();
},

Expand All @@ -192,10 +211,12 @@
detached: function() {
Polymer.dom(this).unobserveNodes(this._observer);
this._observer = null;
if (this.__raf) {
window.cancelAnimationFrame(this.__raf);
this.__raf = null;
for (var cb in this.__rafs) {
if (this.__rafs[cb] !== null) {
cancelAnimationFrame(this.__rafs[cb]);
}
}
this.__rafs = {};
this._manager.removeOverlay(this);

// We got detached while animating, ensure we show/hide the overlay
Expand Down Expand Up @@ -286,8 +307,8 @@

this.__isAnimating = true;

// Use requestAnimationFrame for non-blocking rendering.
this.__onNextAnimationFrame(this.__openedChanged);
// Deraf for non-blocking rendering.
this.__deraf('__openedChanged', this.__openedChanged);
},

_canceledChanged: function() {
Expand Down Expand Up @@ -526,7 +547,7 @@
*/
_onIronResize: function() {
if (this.opened && !this.__isAnimating) {
this.__onNextAnimationFrame(this.refit);
this.__deraf('refit', this.refit);
}
},

Expand Down Expand Up @@ -579,23 +600,108 @@
},

/**
* Executes a callback on the next animation frame, overriding any previous
* callback awaiting for the next animation frame. e.g.
* `__onNextAnimationFrame(callback1) && __onNextAnimationFrame(callback2)`;
* `callback1` will never be invoked.
* @param {!Function} callback Its `this` parameter is the overlay itself.
* Debounces the execution of a callback to the next animation frame.
* @param {!string} jobname
* @param {!Function} callback Always bound to `this`
* @private
*/
__deraf: function(jobname, callback) {
var rafs = this.__rafs;
if (rafs[jobname] !== null) {
cancelAnimationFrame(rafs[jobname]);
}
rafs[jobname] = requestAnimationFrame(function nextAnimationFrame() {
rafs[jobname] = null;
callback.call(this);
}.bind(this));
},

/**
* @param {boolean} isAttached
* @param {boolean} opened
* @param {string=} scrollAction
* @private
*/
__onNextAnimationFrame: function(callback) {
if (this.__raf) {
window.cancelAnimationFrame(this.__raf);
__updateScrollObservers: function(isAttached, opened, scrollAction) {
if (!isAttached || !opened || !this.__isValidScrollAction(scrollAction)) {
Polymer.IronScrollManager.removeScrollLock(this);
document.removeEventListener('scroll', this.__onCaptureScroll, {
passive: true
});
} else {
if (scrollAction === 'lock') {
this.__saveScrollPosition();
Polymer.IronScrollManager.pushScrollLock(this);
}
document.addEventListener('scroll', this.__onCaptureScroll, {
passive: true
});
}
var self = this;
this.__raf = window.requestAnimationFrame(function nextAnimationFrame() {
self.__raf = null;
callback.call(self);
});
}
},

/**
* @param {string=} scrollAction
* @return {boolean}
* @private
*/
__isValidScrollAction: function(scrollAction) {
return scrollAction === 'lock' ||
scrollAction === 'refit' ||
scrollAction === 'cancel';
},

/**
* @private
*/
__onCaptureScroll: function(event) {
if (this.__isAnimating) {
return;
}
switch (this.scrollAction) {
case 'lock':
// NOTE: scrolling might happen if a scroll event is not cancellable, or if
// user pressed keys that cause scrolling (they're not prevented in order not to
// break a11y features like navigate with arrow keys).
this.__restoreScrollPosition();
break;
case 'refit':
this.__deraf('refit', this.refit);
break;
case 'cancel':
this.cancel(event);
break;
}
},

/**
* Memoizes the scroll position of the outside scrolling element.
* @private
*/
__saveScrollPosition: function() {
if (document.scrollingElement) {
this.__scrollTop = document.scrollingElement.scrollTop;
this.__scrollLeft = document.scrollingElement.scrollLeft;
} else {
// Since we don't know if is the body or html, get max.
this.__scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
this.__scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
}
},

/**
* Resets the scroll position of the outside scrolling element.
* @private
*/
__restoreScrollPosition: function() {
if (document.scrollingElement) {
document.scrollingElement.scrollTop = this.__scrollTop;
document.scrollingElement.scrollLeft = this.__scrollLeft;
} else {
// Since we don't know if is the body or html, set both.
document.documentElement.scrollTop = document.body.scrollTop = this.__scrollTop;
document.documentElement.scrollLeft = document.body.scrollLeft = this.__scrollLeft;
}
},

};

Expand Down
Loading

0 comments on commit 6d28420

Please sign in to comment.