Start
-The Scroll Behavior specification allows for native smooth
+ The Scroll
+ Behavior specification allows for
+ native smooth
scrolling
in browsers
– both by using JavaScript scroll APIs like Start
+ window.scrollTo
and
- element.scrollIntoView
or by simply setting the
+ Element.scrollIntoView
or by simply setting the
property
scroll-behavior
to smooth
in CSS, which
will then make any scrolling smooth by default. This includes
@@ -85,59 +88,102 @@ Start
Usage
-Requirements
-Since this script uses the JavaScript scroll APIs and relies on
+ ⚠ Since this script uses the JavaScript scroll APIs and relies on
their smooth scroll functionality to operate, you'll need a
polyfill for the Scroll Behavior spec in order for this script to
make a difference. ⚠ Furthermore, this script assumes that Because browsers don't parse CSS properties they don't recognize.
+ For this reason, reading the Simply define This way, the polyfill can read the
+ property using Alternatively, you can specify the property as the name of a custom
+ font family:
+ Usage
+ smoothscroll-polyfill
is used as example throughout this site, but you may just as well use
another polyfill – or write your own implementation.scroll-behavior
- is set globally to smooth in your CSS. If you don't do that,
- scroll behavior will differ between browsers with and without
- native support for Scroll Behavior. Set it as follows:<style>
-html {
- scroll-behavior: smooth;
-}
-</style>
+
1. Setting
+ scroll-behavior
in CSSscroll-behavior
property
+ from your regular stylesheets is unfortunately not possible (without
+ a performance hit). Instead, specify scroll-behavior
+ using one of these options:Option 1: Using the inline style attribute
+ scroll-behaviour
as an inline style on
+ the html
element:
+ <html style="scroll-behavior: smooth;">
+...
+</html>
getAttribute('style')
even if the browser
+ doesn't parse it.Option 2: Using
+ font-family
as
+ workaround
+ Your actual fonts will still work the way they should – plus, you can
+ simply declare actual fonts on <style>
+ html {
+ /* Normal CSS properties for browsers with native support */
+ scroll-behavior: smooth;
+
+ /* Additionally defined as the name of a font, so the polyfill can read it */
+ font-family: "scroll-behavior: smooth", sans-serif;
+ }
+<style>
body { }
and use font
+ styles on html { }
exclusively for the means of this
+ polyfill, which is prefered. Unlike inline styles, this allows you to
+ use normal CSS features like media queries or classes. The following
+ only enables smooth scroll on desktop devices, for example:
<style>
+ html {
+ scroll-behavior: auto;
+ font-family: "scroll-behavior: auto";
+ }
+ @media screen and (min-width: 1150px) {
+ html {
+ scroll-behavior: smooth;
+ font-family: "scroll-behavior: smooth";
+ }
+ }
+<style>
+ 💡 Redeclaring your scroll-behavior
properties as font
+ names can be automated using a PostCSS
+ plugin, so you can write regular CSS and don't have to
+ bother with font-families. It just works™
Basic usage
+2. Installing the polyfill
+Option 1: Using <script>
Simply drop in <script>
tags linking to the
- polyfills and and you're good to go.
<!-- A polyfill to enable smoothscroll for the JavaScript APIs -->
-<script src="https://unpkg.com/smoothscroll-polyfill"></script>
+ <!-- Any polyfill to enable smoothscroll for the JavaScript APIs -->
+<script src="https://unpkg.com/smoothscroll-polyfill/dist/smoothscroll.min.js"></script>
<!-- This package, to apply the smoothscroll to anchor links -->
<script src="https://unpkg.com/smoothscroll-anchor-polyfill"></script>
- With npm
- Alternatively, if you're using npm, you can
- install using
- npm install smoothscroll-anchor-polyfill
and then use
- the polyfill by
- requiring/importing it in your JS.
-
- // Import any polyfill to enable smoothscroll for JS APIs
+ Option 2: With npm
+
Alternatively, if you're using npm, you can
+ install using npm install smoothscroll-anchor-polyfill
+ and then use the polyfill by requiring/importing it in your JS.
+
+ // Import any polyfill to enable smoothscroll for JS APIs
import smoothscrollPolyfill from 'smoothscroll-polyfill';
+
// Import this package to apply the smoothscroll to anchor links
-import 'smoothscroll-anchor-polyfill';
+import smoothscrollAnchorPolyfill from 'smoothscroll-anchor-polyfill';
-// Enable the main smoothscroll polyfill (might not be neccessary if you use an alternative one)
+// (Unlike this package, smoothscroll-polyfill needs to be actively invoked: )
smoothscrollPolyfill.polyfill();
- Advanced (with Code Splitting)
- If you're using a build system with support for code splitting like
- Parcel or Webpack,
- you can use dynamic imports to load the polyfills – this way,
- browsers won't even download the polyfill code if they already have
- support for the Scroll Behavior spec natively:
-
- // Only continue if polyfills are actually needed
+ Advanced installation (with Code
+ Splitting)
+ If you're using a build system with support for code splitting
+ like
+ Parcel or Webpack,
+ you can use dynamic imports to load the polyfills – this way,
+ browsers won't even download the polyfill code if they already have
+ support for the Scroll Behavior spec natively:
+
+ // Only continue if polyfills are actually needed
if (!('scrollBehavior') in document.documentElement.style) {
// Wait until the Polyfills are loaded
@@ -147,25 +193,41 @@ Advanced (with Code Splitting)
])
// then use the modules however you want
.then(([smoothscrollPolyfill, smoothscrollAnchorPolyfill]) => {
- // e.g. enable the main smoothscroll polyfill (might not be neccessary if you use an alternative one)
+ // (Unlike this package, smoothscroll-polyfill needs to be actively invoked: )
smoothscroll.polyfill();
});
}
Docs
-There's not much more to it than loading this polyfill – it will
+ For 90% of use cases, there should not be much more to it than
+ loading this polyfill – it will
execute immediately no matter if loaded through a script tag or in a
CommonJS environment. If the Scroll Behavior spec is supported
natively, the code won't do anything. ⚠ Remember to also set the The prefered way to dynamically adjust the scroll behavior is the font-family workaround. This way you can
+ simply toggle a CSS class on You can also assign these values directly to
+ ⚠ Assigning to This package exports two methods, The polyfill runs automatically when it's loaded, setting up
+ the EventListeners it needs. This method disables the
+ polyfill and removes all EventListeners. If you used ⚠ Note that both the global force flag
+ and the
+ check for native
+ support ( As already explained in the Usage section,
+ In browsers with native support, you can define This actually doesn't have anything to do with this polyfill –
+ it's a limitation of Blink's native implementation (so it affects
+ other Blink-based browsers like Opera, too). While 'normal'
+ scrolling is smooth, if you click a couple of links and then
+ navigate back and forth using the browser's forwards/backwards
+ buttons (which triggers a No. The polyfill uses Event
Delegation to detect clicks,
so even if an anchor is added to the page after the polyfill was
loaded, everything should work. There currently is no way to disable the polyfill once it was
- loaded. I considered adding this functionality but couldn't think of
- a good use case – if you have one, let me know! 💡 Note that even if global No. Hinweis: die folgenden rechtlichen Hinweise betreffen
diese Website als solche entsprechend deutschem und europäischem
Recht, nicht das vorgestellte Software-Paket "smoothscroll-anchor-polyfill".
@@ -329,11 +474,12 @@ Note: the legalities discussed here concern this website
itself, not the software package "smoothscroll-anchor-polyfill",
and are required for compliance with German & European law. "smoothscroll-anchor-polyfill"
- itself is licensed under MIT license, for more information
+ itself is licensed under a plain MIT license, for more
+ information
check out the respective GitHub
repository.Docs
+ scroll-behavior
property
- globally (on html
)
- in your CSS, otherwise your site will behave differently in browsers
- with and without support for the Scroll Behavior spec! ⚠Use the Polyfill even if there is native
- support
+ Changing the scroll behavior
+ <html>
depending on the
+ behavior you want. Valid property values are smooth
for
+ enabling smooth scroll and auto
, initial
,
+ inherit
or unset
for enabling instant,
+ jumping scroll.document.documentElement.style.scrollBehavior
,
+ it will have precedence over both the inline style attribute and
+ the
+ property set using the font-family workaround..scrollBehavior
is not recommened
+ however as this property is used for feature detection. Assigning to
+ it before a polyfill was loaded will break this one and most other
+ polyfills related to smooth scrolling. ⚠Using the polyfill even if there is native
+ support
window.__forceSmoothscrollAnchorPolyfill__
:
@@ -176,29 +238,112 @@ Use the Polyfill even if there is native
native smooth scroll. Not recommended.
Polyfill anchors dynamically inserted later
+ Methods:
+ destroy
and polyfill
destroy
and polyfill
.
+ If loaded through a script tag, these methods are exposed on
+ window.smoothscrollAnchorPolyfill
.destroy()
:
+ polyfill({ force: boolean })
:
+ destroy()
to disable the polyfill,
+ you can re-enable it with this method. It takes an (optional)
+ Object as argument, the property force
behaves
+ the same way as the global force flag
+ on window
.'scrollBehavior' in document.documentElement.style
)
+ will be re-evaluated when polyfill()
runs. If you
+ assigned to .scrollBehavior
in the
+ meantime, this check will evaluate to true
and the
+ polyfill won't enable itself.
+ Use the force flag or run delete document.documentElement.style.scrollBehavior;
+ if you encounter this problem.Limitations
+
+ scroll-behavior
is
+ not
+ detected in regular
+ stylesheetsscroll-behavior
+ can not be set in regular CSS, as accessing the property there
+ from JavaScript is not possible without a performance hit. This
+ is caused by browsers not parsing a CSS property if it isn't
+ recognized as valid. If you need the flexiblity of CSS, consider
+ the font-family
+ workaround.
+ scroll-behavior
is only
+ supported
+ as global settingscroll-behavior
+ at multiple points in your document, e.g. auto
on
+ the document itself, but smooth
on a slideshow
+ container that has separate scrolling. This polyfill does not
+ allow for that, either all anchors on the page scroll smoothly by
+ setting scroll-behavior
+ at document level, or none.Scrolling triggered by
+ hashchange
+ is not smooth in Chromehashchange
+ everytime), it jumps from anchor to anchor instead of scrolling
+ smoothly. If this is important to you, you can fix it by
+ detecting the Blink engine and force-enabling this polyfill. Load
+ browsengine.js,
+ then do (before the polyfill runs):
+ if (window.webpage.engine.blink) {
+ window.__forceSmoothscrollAnchorPolyfill__ = true;
+}
FAQ
+ Will this break Server Side Rendering?
+ Polyfill anchors dynamically inserted
+ later
Disable the polyfill
- scroll-behavior
is set and
- this polyfill is loaded, you can still perform instant, jumping
- navigation by using one of the JavaScript scroll APIs and specifying
- { behavior: 'instant' }
in the options object.Will this break Server Side Rendering?
- Does this support
+ prefers-reduced-motion
?prefers-reduced-motion
is a relatively new CSS media
+ query that hints at whether a client prefers less motion, which can
+ be important for people with certain illnesses. But Safari is the
+ only browser that has implemented it yet, and it doesn't support the
+ Scroll Behavior spec, so there is no reference for the interplay of
+ prefers-reduced-motion
and Scroll Behavior yet. For this
+ reason, it is not implemented (yet) in this polyfill.
+ However, it is relatively safe to assume that prefers-reduced-motion
+ will disable scroll-behavior: smooth
so this can
+ absolutely be discussed – please file an issue on GitHub if it
+ affects your project.Rechtliches
+ Rechtliches
Datenschutz
veranlassen.Legal
+ Legal
Imprint
@@ -418,9 +564,9 @@ Privacy
diff --git a/index.js b/index.js
index 7e59a05..7f3c96e 100755
--- a/index.js
+++ b/index.js
@@ -4,268 +4,278 @@
* (c) 2018 Jonas Kuske
* Released under the MIT License.
*/
-(function (indexFn) {
- if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
- // Test env: export function so it can run multiple times
- exports.polyfill = indexFn;
- } else {
- // Else run immediately, no time to waste!
- indexFn();
- }
-})(function () {
-
- /* */
-
- // Noop to be returned if polyfill returns early. Will be assigned later
- var destroy = function () { };
-
- // Abort if outside browser (window is undefined in Node etc.)
- var isBrowser = typeof window !== 'undefined';
- if (!isBrowser) return destroy;
-
- var w = window, d = document, docEl = d.documentElement;
-
- var forcePolyfill = w.__forceSmoothscrollAnchorPolyfill__ === true;
- // Abort if smoothscroll is natively supported and force flag is not set
- if (!forcePolyfill && 'scrollBehavior' in docEl.style) return destroy;
-
- // Check if browser supports focus without automatic scrolling (preventScroll)
- var supportsPreventScroll = false;
- try {
- var el = d.createElement('a');
- // Define getter for preventScroll to find out if the browser accesses it
- var preppedFocusOption = Object.defineProperty({}, 'preventScroll', {
- get: function () {
- supportsPreventScroll = true;
- }
- });
- // Trigger focus – if browser uses preventScroll the var will be set to true
- el.focus(preppedFocusOption);
- } catch (e) { }
-
- // Regex to extract the value following the scroll-behavior property name
- var extractValue = /scroll-behavior:[\s]*([^;"`'\s]+)/;
-
- /**
- * Returns true if scroll-behavior: smooth is set and not overwritten
- * by a higher-specifity declaration, else returns false
- */
- function shouldSmoothscroll() {
- // Values to check for set scroll-behavior in order of priority/specificity
- var valuesToCheck = [
- // Priority 1: behavior specified as inline property
- // Allows toggling smoothscroll from JS (docEl.style.scrollBehavior = ...)
- docEl.style.scrollBehavior,
- // Priority 2: behavior specified inline in style attribute
- (extractValue.exec(docEl.getAttribute('style')) || [])[1],
- // Priority 3: behavior specified in fontFamily
- // Behaves like regular CSS, e.g. allows using media queries
- (extractValue.exec(getComputedStyle(docEl).fontFamily) || [])[1]
- ];
- // Loop over values in specified order, return once a valid value is found
- for (var i = 0; i < valuesToCheck.length; i++) {
- var specifiedBehavior = getScrollBehavior(valuesToCheck[i]);
- if (specifiedBehavior !== null) return specifiedBehavior;
+
+(function(global, factory) {
+ var SmoothscrollAnchorPolyfill = factory();
+ if (typeof exports === 'object' && typeof module !== 'undefined') {
+ module.exports = SmoothscrollAnchorPolyfill;
+ } else {
+ global.SmoothscrollAnchorPolyfill = SmoothscrollAnchorPolyfill;
+ }
+
+ // In test environment: abort, else run polyfill immediately
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return;
+ else SmoothscrollAnchorPolyfill.polyfill();
+})(this, function() {
+
+ /* */
+
+ // Abort if outside browser (window is undefined in Node etc.)
+ var isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) return;
+
+ var w = window, d = document, docEl = d.documentElement;
+
+ // Check if browser supports focus without automatic scrolling (preventScroll)
+ var supportsPreventScroll = false;
+ try {
+ var el = d.createElement('a');
+ // Define getter for preventScroll to find out if the browser accesses it
+ var preppedFocusOption = Object.defineProperty({}, 'preventScroll', {
+ get: function() {
+ supportsPreventScroll = true;
+ }
+ });
+ // Trigger focus – if browser uses preventScroll the var will be set to true
+ el.focus(preppedFocusOption);
+ } catch (e) { }
+
+ // Regex to extract the value following the scroll-behavior property name
+ var extractValue = /scroll-behavior:[\s]*([^;"`'\s]+)/;
+
+ /**
+ * Returns true if scroll-behavior: smooth is set and not overwritten
+ * by a higher-specifity declaration, else returns false
+ */
+ function shouldSmoothscroll() {
+ // Values to check for set scroll-behavior in order of priority/specificity
+ var valuesToCheck = [
+ // Priority 1: behavior specified as inline property
+ // Allows toggling smoothscroll from JS (docEl.style.scrollBehavior = ...)
+ docEl.style.scrollBehavior,
+ // Priority 2: behavior specified inline in style attribute
+ (extractValue.exec(docEl.getAttribute('style')) || [])[1],
+ // Priority 3: behavior specified in fontFamily
+ // Behaves like regular CSS, e.g. allows using media queries
+ (extractValue.exec(getComputedStyle(docEl).fontFamily) || [])[1]
+ ];
+ // Loop over values in specified order, return once a valid value is found
+ for (var i = 0; i < valuesToCheck.length; i++) {
+ var specifiedBehavior = getScrollBehavior(valuesToCheck[i]);
+ if (specifiedBehavior !== null) return specifiedBehavior;
+ }
+ // No value found? Return false, no set value = no smoothscroll :(
+ return false;
+ }
+
+ /**
+ * If a valid CSS property value for scroll-behavior is passed, returns
+ * whether it specifies smooth scroll behavior or not, else returns null
+ * @param {any} value The value to check
+ * @returns {?boolean} The specified scroll behavior or null
+ */
+ function getScrollBehavior(value) {
+ var status = null;
+ if (/^smooth$/.test(value)) status = true;
+ if (/^(initial|inherit|auto|unset)$/.test(value)) status = false;
+ return status;
+ }
+
+ /**
+ * Get the target element of an event
+ * @param {event} evt
+ */
+ function getEventTarget(evt) {
+ evt = evt || w.event;
+ return evt.target || evt.srcElement;
+ }
+
+ /**
+ * Check if an element is an anchor pointing to a target on the current page
+ * @param {HTMLElement} el
+ */
+ function isAnchorToLocalElement(el) {
+ return (
+ // Is an anchor with a fragment in the url
+ el.tagName && el.tagName.toLowerCase() === 'a' && /#/.test(el.href) &&
+ // Target is on current page
+ el.hostname === location.hostname && el.pathname === location.pathname
+ );
+ }
+
+ /**
+ * Focuses an element, if it's not focused after the first try,
+ * allow focusing by adjusting tabIndex and retry
+ * @param {HTMLElement} el
+ */
+ function focusElement(el) {
+ el.focus({ preventScroll: true });
+ if (d.activeElement !== el) {
+ el.setAttribute('tabindex', '-1');
+ // TODO: Only remove outline if it comes from the UA, not the user CSS
+ el.style.outline = 'none';
+ el.focus({ preventScroll: true });
+ }
+ }
+
+ /**
+ * Returns the element whose id matches the hash or
+ * document.body if the hash is "#top" or "" (empty string)
+ * @param {string} hash
+ */
+ function getScrollTarget(hash) {
+ if (typeof hash !== 'string') return null;
+
+ // Retrieve target if an id is specified in the hash, otherwise use body.
+ // If hash is "#top" and no target with id "top" was found, also use body
+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href
+ var target = hash ? d.getElementById(hash.slice(1)) : d.body;
+ if (hash === '#top' && !target) target = d.body;
+ return target;
+ }
+
+ /**
+ * Walks up the DOM starting from a given element until an element satisfies the validate function
+ * @param {HTMLElement} element The element from where to start validating
+ * @param {Function} validate The validation function
+ * @returns {HTMLElement|boolean}
+ */
+ function findInParents(element, validate) {
+ if (validate(element)) return element;
+ if (element.parentNode) return findInParents(element.parentNode, validate);
+ return false;
+ }
+
+ // Stores the setTimeout id of pending focus changes, allows aborting them
+ var pendingFocusChange;
+
+ /**
+ * Scrolls to a given element or to the top if the given element
+ * is document.body, then focuses the element
+ * @param {HTMLElement} target
+ */
+ function triggerSmoothscroll(target) {
+ // Clear potential pending focus change triggered by a previous scroll
+ if (!supportsPreventScroll) clearTimeout(pendingFocusChange);
+
+ // Use JS scroll APIs to scroll to top (if target is body) or to the element
+ // This allows polyfills for these APIs to do their smooth scrolling magic
+ var scrollTop = target === d.body;
+ if (scrollTop) w.scroll({ top: 0, left: 0, behavior: 'smooth' });
+ else target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+
+ // If the browser supports preventScroll: immediately focus the target
+ // Otherwise schedule the focus so the smoothscroll isn't interrupted
+ if (supportsPreventScroll) focusElement(target);
+ else pendingFocusChange = setTimeout(focusElement.bind(null, target), 450);
+ }
+
+ /**
+ * Check if the clicked target is an anchor pointing to a local element,
+ * if so prevent the default behavior and handle the scrolling using the
+ * native JavaScript scroll APIs so smoothscroll polyfills apply
+ * @param {event} evt
+ */
+ function handleClick(evt) {
+ // Abort if shift/ctrl-click or not primary click (button !== 0)
+ if (evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.button !== 0) return;
+ // scroll-behavior not set to smooth? Bail out, let browser handle it
+ if (!shouldSmoothscroll()) return;
+
+ // Check the DOM from the click target upwards if a local anchor was clicked
+ var anchor = findInParents(getEventTarget(evt), isAnchorToLocalElement);
+ if (!anchor) return;
+
+ // Find the element targeted by the hash
+ var hash = anchor.hash;
+ var target = getScrollTarget(hash);
+
+ if (target) {
+ // Prevent default browser behavior to avoid a jump to the anchor target
+ evt.preventDefault();
+
+ // Trigger the smooth scroll
+ triggerSmoothscroll(target);
+
+ // Append the hash to the URL
+ if (history.pushState) history.pushState(null, d.title, (hash || '#'));
+ }
+
+ }
+
+ // To enable smooth scrolling on hashchange, we need to immediately restore
+ // the scroll pos after a hashchange changed it, so we track it constantly.
+ // Some browsers don't trigger a scroll event before the hashchange,
+ // so to undo, the position from last scroll is the one we need to go back to.
+ // In others (e.g. IE) the scroll listener is triggered again before the
+ // hashchange occurs and the last reported position is already the new one
+ // updated by the hashchange – we need the second last to undo there.
+ // Because of this we don't track just the last, but the last two positions.
+ var lastTwoScrollPos = [];
+
+ /**
+ * Tries to undo the automatic, instant scroll caused by a hashchange
+ * and instead scrolls smoothly to the new hash target
+ */
+ function handleHashChange() {
+ // scroll-behavior not set to smooth or body nor parsed yet? Abort
+ if (!shouldSmoothscroll() || !d.body) return;
+
+ var target = getScrollTarget(location.hash);
+ if (!target) return;
+
+ // If the position last reported by the scroll listener is the same as the
+ // current one caused by a hashchange, go back to second last – else last
+ var currentPos = getScrollTop();
+ var top = lastTwoScrollPos[lastTwoScrollPos[1] === currentPos ? 0 : 1];
+
+ // Undo the scroll caused by the hashchange...
+ w.scroll({ top: top, behavior: 'instant' });
+ // ...and instead smoothscroll to the target
+ triggerSmoothscroll(target);
}
- // No value found? Return false, no set value = no smoothscroll :(
- return false;
- }
-
- /**
- * If a valid CSS property value for scroll-behavior is passed, returns
- * whether it specifies smooth scroll behavior or not, else returns null
- * @param {any} value The value to check
- * @returns {boolean|undefined}
- */
- function getScrollBehavior(value) {
- var status = null;
- if (/^smooth$/.test(value)) status = true;
- if (/^(initial|inherit|auto|unset)$/.test(value)) status = false;
- return status;
- }
-
- /**
- * Get the target element of an event
- * @param {event} evt
- */
- function getEventTarget(evt) {
- evt = evt || w.event;
- return evt.target || evt.srcElement;
- }
-
- /**
- * Check if an element is an anchor pointing to a target on the current page
- * @param {HTMLElement} el
- */
- function isAnchorToLocalElement(el) {
- return (
- // Is an anchor
- el.tagName && el.tagName.toLowerCase() === 'a' &&
- // Targets an element
- el.href.indexOf('#') > -1 &&
- // Target is on current page
- el.hostname === location.hostname && el.pathname === location.pathname
- );
- }
-
- /**
- * Focuses an element, if it's not focused after the first try,
- * allow focusing by adjusting tabIndex and retry
- * @param {HTMLElement} el
- */
- function focusElement(el) {
- el.focus({ preventScroll: true });
- if (d.activeElement !== el) {
- el.setAttribute('tabindex', '-1');
- // TODO: Only remove outline if it comes from the UA, not the user CSS
- el.style.outline = 'none';
- el.focus({ preventScroll: true });
+
+ /**
+ * Returns the scroll offset towards the top
+ */
+ function getScrollTop() {
+ return docEl.scrollTop || d.body.scrollTop;
}
- }
-
- /**
- * Returns the element whose id matches the hash or
- * document.body if the hash is "#top" or "" (empty string)
- * @param {string} hash
- */
- function getScrollTarget(hash) {
- if (typeof hash !== 'string') return null;
-
- // Retrieve target if an id is specified in the hash, otherwise use body.
- // If hash is "#top" and no target with id "top" was found, also use body
- // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href
- var target = hash ? d.getElementById(hash.slice(1)) : d.body;
- if (hash === '#top' && !target) target = d.body;
- return target;
- }
-
- /**
- * Walks up the DOM starting from a given element until an element satisfies the validate function
- * @param {HTMLElement} element The element from where to start validating
- * @param {Function} validate The validation function
- * @returns {HTMLElement|boolean}
- */
- function findInParents(element, validate) {
- if (validate(element)) return element;
- if (element.parentNode) return findInParents(element.parentNode, validate);
- return false;
- }
-
- // Stores the setTimeout id of pending focus changes, allows aborting them
- var pendingFocusChange;
-
- /**
- * Scrolls to a given element or to the top if the given element
- * is document.body, then focuses the element
- * @param {HTMLElement} target
- */
- function triggerSmoothscroll(target) {
- // Clear potential pending focus change triggered by a previous scroll
- if (!supportsPreventScroll) clearTimeout(pendingFocusChange);
-
- // Use JS scroll APIs to scroll to top (if target is body) or to the element
- // This allows polyfills for these APIs to do their smooth scrolling magic
- var scrollTop = target === d.body;
- if (scrollTop) w.scroll({ top: 0, left: 0, behavior: 'smooth' });
- else target.scrollIntoView({ behavior: 'smooth', block: 'start' });
-
- // If the browser supports preventScroll: immediately focus the target
- // Otherwise schedule the focus so the smoothscroll isn't interrupted
- if (supportsPreventScroll) focusElement(target);
- else pendingFocusChange = setTimeout(focusElement.bind(null, target), 450);
- }
-
- /**
- * Check if the clicked target is an anchor pointing to a local element,
- * if so prevent the default behavior and handle the scrolling using the
- * native JavaScript scroll APIs so smoothscroll polyfills apply
- * @param {event} evt
- */
- function handleClick(evt) {
- // Abort if shift/ctrl-click or not primary click (button !== 0)
- if (evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.button !== 0) return;
- // scroll-behavior not set to smooth? Bail out, let browser handle it
- if (!shouldSmoothscroll()) return;
-
- // Check the DOM from the click target upwards if a local anchor was clicked
- var anchor = findInParents(getEventTarget(evt), isAnchorToLocalElement);
- if (!anchor) return;
-
- // Find the element targeted by the hash
- var hash = anchor.hash;
- var target = getScrollTarget(hash);
-
- if (target) {
- // Prevent default browser behavior to avoid a jump to the anchor target
- evt.preventDefault();
-
- // Trigger the smooth scroll
- triggerSmoothscroll(target);
-
- // Append the hash to the URL
- if (history.pushState) history.pushState(null, d.title, (hash || '#'));
+
+ /**
+ * Update the last two scroll positions
+ */
+ function trackScrollPositions() {
+ if (!d.body) return; // Body not parsed yet? Abort
+ lastTwoScrollPos[0] = lastTwoScrollPos[1];
+ lastTwoScrollPos[1] = getScrollTop();
}
- }
-
- // To enable smooth scrolling on hashchange, we need to immediately restore
- // the scroll pos after a hashchange changed it, so we track it constantly.
- // Some browsers don't trigger a scroll event before the hashchange,
- // so to undo, the position from last scroll is the one we need to go back to.
- // In others (e.g. IE) the scroll listener is triggered again before the
- // hashchange occurs and the last reported position is already the new one
- // updated by the hashchange – we need the second last to undo there.
- // Because of this we don't track just the last, but the last two positions.
- var lastTwoScrollPos = [];
-
- /**
- * Tries to undo the automatic, instant scroll caused by a hashchange
- * and instead scrolls smoothly to the new hash target
- */
- function handleHashChange() {
- // scroll-behavior not set to smooth or body nor parsed yet? Abort
- if (!shouldSmoothscroll() || !d.body) return;
-
- var target = getScrollTarget(location.hash);
- if (!target) return;
-
- // If the position last reported by the scroll listener is the same as the
- // current one caused by a hashchange, go back to second last – else last
- var currentPos = getScrollTop();
- var top = lastTwoScrollPos[lastTwoScrollPos[1] === currentPos ? 0 : 1];
-
- // Undo the scroll caused by the hashchange...
- w.scroll({ top: top, behavior: 'instant' });
- // ...and instead smoothscroll to the target
- triggerSmoothscroll(target);
- }
-
- /**
- * Returns the scroll offset towards the top
- */
- function getScrollTop() {
- return docEl.scrollTop || d.body.scrollTop;
- }
-
- /**
- * Update the last two scroll positions
- */
- function trackScrollPositions() {
- if (!d.body) return; // Body not parsed yet? Abort
- lastTwoScrollPos[0] = lastTwoScrollPos[1];
- lastTwoScrollPos[1] = getScrollTop();
- }
-
- // Register all event handlers
- d.addEventListener('click', handleClick, false);
- d.addEventListener('scroll', trackScrollPositions);
- w.addEventListener("hashchange", handleHashChange);
-
- // Assign destroy function that unregisters event listeners. Used for testing
- destroy = function () {
- d.removeEventListener('click', handleClick, false);
- d.removeEventListener('scroll', trackScrollPositions);
- w.removeEventListener('hashchange', handleHashChange);
- }
- return destroy;
+ return {
+ /**
+ * Starts the polyfill by attaching the neccessary EventListeners
+ *
+ * Aborts, if ('scrollBehavior' in documentElement.style) and the force flag
+ * isn't set on the options parameter Object or globally on window
+ * @param {{force: boolean}} opts Enable polyfill despite native support
+ */
+ polyfill: function(opts) {
+ opts = opts || {};
+ // Abort if smoothscroll is natively supported and force flag is not set
+ var forcePolyfill = opts.force || w.__forceSmoothscrollAnchorPolyfill__;
+ if (!forcePolyfill && 'scrollBehavior' in docEl.style) return;
+
+ d.addEventListener('click', handleClick, false);
+ d.addEventListener('scroll', trackScrollPositions);
+ w.addEventListener('hashchange', handleHashChange);
+ },
+ /**
+ * Stops the polyfill by removing all EventListeners
+ */
+ destroy: function() {
+ d.removeEventListener('click', handleClick, false);
+ d.removeEventListener('scroll', trackScrollPositions);
+ w.removeEventListener('hashchange', handleHashChange);
+ }
+ };
});
diff --git a/package.json b/package.json
index f7b29bc..a1b4100 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "smoothscroll-anchor-polyfill",
- "version": "1.0.0-beta",
+ "version": "1.0.0",
"description": "Apply smooth scroll to anchor links to replicate CSS scroll-behavior",
"main": "dist/index.js",
"browserslist": [
diff --git a/test.js b/test.js
index b83e55d..2b19f90 100755
--- a/test.js
+++ b/test.js
@@ -1,4 +1,4 @@
-const { polyfill } = require('./index')
+const { polyfill, destroy } = require('./index')
const insertElement = (type, attrs) => {
const el = document.createElement(type)
@@ -22,7 +22,7 @@ afterEach(() => {
document.documentElement.removeAttribute('style')
document.body.innerHTML = ''
- document.head.innerHTML = ''
+ destroy()
})
describe('General', () => {
@@ -33,15 +33,14 @@ describe('General', () => {
document.documentElement.style.scrollBehavior = 'smooth'
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).not.toHaveBeenCalled()
- destroy()
})
- it('Runs even in browsers with native support if force flag is set', () => {
+ it('Runs even with native support if force flag is set on window', () => {
const anchor = insertElement('a', { href: '#' })
// Mock native support
@@ -50,11 +49,24 @@ describe('General', () => {
window.__forceSmoothscrollAnchorPolyfill__ = true
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
+
+ anchor.click()
+ expect(spy).toHaveBeenCalled()
+ })
+
+ it('Runs even with native support if force flag is passed as arg', () => {
+ const anchor = insertElement('a', { href: '#' })
+
+ // Mock native support
+ document.documentElement.style.scrollBehavior = 'smooth'
+
+ const spy = jest.spyOn(window, 'scroll')
+ // Pass force flag in options object
+ polyfill({ force: true })
anchor.click()
expect(spy).toHaveBeenCalled()
- destroy()
})
})
@@ -66,12 +78,11 @@ describe('Scroll targeting', () => {
const anchorTwo = insertElement('a', { href: '#' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
anchorTwo.click()
expect(spy).toHaveBeenCalledTimes(2)
- destroy()
})
it('Scrolls to targeted element if anchor targets its ID', () => {
@@ -82,12 +93,11 @@ describe('Scroll targeting', () => {
const windowSpy = jest.spyOn(window, 'scroll')
const spy = jest.spyOn(target, 'scrollIntoView')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(windowSpy).not.toHaveBeenCalled()
expect(spy).toHaveBeenCalled()
- destroy()
})
it('Scrolls to element instead of top if hash "#top" targets an ID', () => {
@@ -98,12 +108,11 @@ describe('Scroll targeting', () => {
const windowSpy = jest.spyOn(window, 'scroll')
const spy = jest.spyOn(target, 'scrollIntoView')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalled()
expect(windowSpy).not.toHaveBeenCalled()
- destroy()
})
})
@@ -115,11 +124,10 @@ describe('Enabling & Toggling the polyfill', () => {
const anchor = insertElement('a', { href: '#' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalled()
- destroy()
})
it('Can be enabled through the style attribute on ', () => {
@@ -128,18 +136,17 @@ describe('Enabling & Toggling the polyfill', () => {
const anchor = insertElement('a', { href: '#' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalled()
- destroy()
})
it('Can be enabled by documentElement.style.scrollBehavior', () => {
const anchor = insertElement('a', { href: '#' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
// Only set scrollBehavior after the polyfill ran, so it doesn't bail
// due to checking for native support ('scrollBehavior' in docEl.style)
@@ -147,7 +154,6 @@ describe('Enabling & Toggling the polyfill', () => {
anchor.click()
expect(spy).toHaveBeenCalled()
- destroy()
})
it('Can be disabled through method with higher specificity', () => {
@@ -157,7 +163,7 @@ describe('Enabling & Toggling the polyfill', () => {
const anchor = insertElement('a', { href: '#' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalledTimes(1)
@@ -169,7 +175,6 @@ describe('Enabling & Toggling the polyfill', () => {
document.documentElement.style.scrollBehavior = 'smooth'
anchor.click()
expect(spy).toHaveBeenCalledTimes(2)
- destroy()
})
})
@@ -180,21 +185,20 @@ describe('Enabling & Toggling the polyfill', () => {
const anchor = insertElement('a', { href: '#top' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalledTimes(1)
document.documentElement.style.scrollBehavior = 'auto'
anchor.click()
expect(spy).toHaveBeenCalledTimes(1)
- destroy()
})
it('Responds to computedStyle of font-family', () => {
const anchor = insertElement('a', { href: '#top' })
const spy = jest.spyOn(window, 'scroll')
- const destroy = polyfill()
+ polyfill()
anchor.click()
expect(spy).toHaveBeenCalledTimes(0)
@@ -204,7 +208,6 @@ describe('Enabling & Toggling the polyfill', () => {
document.documentElement.style.font = '100 1rem "scroll-behavior:inherit"'
anchor.click()
expect(spy).toHaveBeenCalledTimes(1)
- destroy()
})
})
})
\ No newline at end of file