From: Brett Mason Date: Thu, 10 Nov 2016 20:22:00 +0000 (+0000) Subject: Rewrite of off canvas Sass and JavaScript. Initial commit, still needs tidying up. X-Git-Tag: v6.3-rc1~6^2~13^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f34f72428f7c3bfd34f7c089b9ba808a75cb09e2;p=thirdparty%2Ffoundation%2Ffoundation-sites.git Rewrite of off canvas Sass and JavaScript. Initial commit, still needs tidying up. --- diff --git a/js/foundation.offcanvas.js b/js/foundation.offcanvas.js index b1676253d..9c1286f1d 100644 --- a/js/foundation.offcanvas.js +++ b/js/foundation.offcanvas.js @@ -27,11 +27,7 @@ class OffCanvas { this._init(); this._events(); - Foundation.registerPlugin(this, 'OffCanvas') - Foundation.Keyboard.register('OffCanvas', { - 'ESCAPE': 'close' - }); - + Foundation.registerPlugin(this, 'OffCanvas'); } /** @@ -44,6 +40,8 @@ class OffCanvas { this.$element.attr('aria-hidden', 'true'); + this.$element.addClass(`is-transition-${this.options.transition}`); + // Find triggers that affect this element and add aria-expanded to them this.$triggers = $(document) .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]') @@ -51,15 +49,15 @@ class OffCanvas { .attr('aria-controls', id); // Add a close trigger over the body if necessary - if (this.options.closeOnClick) { + if (this.options.contentOverlay) { if ($('.js-off-canvas-exit').length) { - this.$exiter = $('.js-off-canvas-exit'); + this.$overlay = $('.js-off-canvas-exit'); } else { - var exiter = document.createElement('div'); - exiter.setAttribute('class', 'js-off-canvas-exit'); - $('[data-off-canvas-content]').append(exiter); + var overlay = document.createElement('div'); + overlay.setAttribute('class', 'js-off-canvas-exit'); + $('[data-off-canvas-content]').append(overlay); - this.$exiter = $(exiter); + this.$overlay = $(overlay); } } @@ -70,7 +68,7 @@ class OffCanvas { this._setMQChecker(); } if (!this.options.transitionTime) { - this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas-wrapper]')[0]).transitionDuration) * 1000; + this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas-content]')[0]).transitionDuration) * 1000; } } @@ -87,8 +85,14 @@ class OffCanvas { 'keydown.zf.offcanvas': this._handleKeyboard.bind(this) }); - if (this.options.closeOnClick && this.$exiter.length) { - this.$exiter.on({'click.zf.offcanvas': this.close.bind(this)}); + // If we have an overlay and close on click, let it close the off canvas menu. + if (this.options.closeOnClick && this.options.contentOverlay && this.$overlay.length) { + this.$overlay.on({'click.zf.offcanvas': this.close.bind(this)}); + } + + // If content overlay is false but close on click is true, close via click on content. + if (this.options.closeOnClick && this.options.contentOverlay !== true) { + $('[data-off-canvas-content]').on({'click.zf.offcanvas': this.close.bind(this)}); } } @@ -122,19 +126,10 @@ class OffCanvas { if (isRevealed) { this.close(); this.isRevealed = true; - // if (!this.options.forceTop) { - // var scrollPos = parseInt(window.pageYOffset); - // this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)'; - // } - // if (this.options.isSticky) { this._stick(); } this.$element.off('open.zf.trigger toggle.zf.trigger'); if ($closer.length) { $closer.hide(); } } else { this.isRevealed = false; - // if (this.options.isSticky || !this.options.forceTop) { - // this.$element[0].style.transform = ''; - // $(window).off('scroll.zf.offcanvas'); - // } this.$element.on({ 'open.zf.trigger': this.open.bind(this), 'toggle.zf.trigger': this.toggle.bind(this) @@ -157,38 +152,33 @@ class OffCanvas { var _this = this, $body = $(document.body); - if (this.options.forceTop) { - $('body').scrollTop(0); + if (this.options.forceTo === 'top') { + window.scrollTo(0, 0); + } else if (this.options.forceTo === 'bottom') { + window.scrollTo(0,document.body.scrollHeight); } - // window.pageYOffset = 0; - - // if (!this.options.forceTop) { - // var scrollPos = parseInt(window.pageYOffset); - // this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)'; - // if (this.$exiter.length) { - // this.$exiter[0].style.transform = 'translate(0,' + scrollPos + 'px)'; - // } - // } + /** * Fires when the off-canvas menu opens. * @event OffCanvas#opened */ - - var $wrapper = $('[data-off-canvas-wrapper]'); - $wrapper.addClass('is-off-canvas-open is-open-'+ _this.options.position); - - _this.$element.addClass('is-open') - - // if (_this.options.isSticky) { - // _this._stick(); - // } + Foundation.Move(this.options.transitionTime, this.$element, function() { + $('body').addClass('is-off-canvas-open'); + _this.$element.addClass('is-open') + }); this.$triggers.attr('aria-expanded', 'true'); this.$element.attr('aria-hidden', 'false') .trigger('opened.zf.offcanvas'); - if (this.options.closeOnClick) { - this.$exiter.addClass('is-visible'); + // If we have an overlay lets make it visible. + if (this.options.contentOverlay) { + this.$overlay.addClass('is-visible'); + } + + // If we have close on click and an overlay add a `is-closable` class. + if (this.options.closeOnClick && this.options.contentOverlay) { + this.$overlay.addClass('is-closable'); } if (trigger) { @@ -196,21 +186,14 @@ class OffCanvas { } if (this.options.autoFocus) { - $wrapper.one(Foundation.transitionend($wrapper), function() { - if(_this.$element.hasClass('is-open')) { // handle double clicks - _this.$element.attr('tabindex', '-1'); - _this.$element.focus(); - } + this.$element.one(Foundation.transitionend(this.$element), function() { + _this.$element.find('a, button').eq(0).focus(); }); } if (this.options.trapFocus) { - $wrapper.one(Foundation.transitionend($wrapper), function() { - if(_this.$element.hasClass('is-open')) { // handle double clicks - _this.$element.attr('tabindex', '-1'); - _this._trapFocus(); - } - }); + $('[data-off-canvas-content]').attr('tabindex', '-1'); + this._trapFocus(); } } @@ -224,37 +207,19 @@ class OffCanvas { last = focusable.eq(-1); focusable.off('.zf.offcanvas').on('keydown.zf.offcanvas', function(e) { - var key = Foundation.Keyboard.parseKey(e); - if (key === 'TAB' && e.target === last[0]) { - e.preventDefault(); - first.focus(); - } - if (key === 'SHIFT_TAB' && e.target === first[0]) { - e.preventDefault(); - last.focus(); + if (e.which === 9 || e.keycode === 9) { + if (e.target === last[0] && !e.shiftKey) { + e.preventDefault(); + first.focus(); + } + if (e.target === first[0] && e.shiftKey) { + e.preventDefault(); + last.focus(); + } } }); } - /** - * Allows the offcanvas to appear sticky utilizing translate properties. - * @private - */ - // OffCanvas.prototype._stick = function() { - // var elStyle = this.$element[0].style; - // - // if (this.options.closeOnClick) { - // var exitStyle = this.$exiter[0].style; - // } - // - // $(window).on('scroll.zf.offcanvas', function(e) { - // console.log(e); - // var pageY = window.pageYOffset; - // elStyle.transform = 'translate(0,' + pageY + 'px)'; - // if (exitStyle !== undefined) { exitStyle.transform = 'translate(0,' + pageY + 'px)'; } - // }); - // // this.$element.trigger('stuck.zf.offcanvas'); - // }; /** * Closes the off-canvas menu. * @function @@ -266,25 +231,24 @@ class OffCanvas { var _this = this; - // Foundation.Move(this.options.transitionTime, this.$element, function() { - $('[data-off-canvas-wrapper]').removeClass(`is-off-canvas-open is-open-${_this.options.position}`); + $('body').removeClass('is-off-canvas-open'); _this.$element.removeClass('is-open'); - // Foundation._reflow(); - // }); + this.$element.attr('aria-hidden', 'true') /** * Fires when the off-canvas menu opens. * @event OffCanvas#closed */ .trigger('closed.zf.offcanvas'); - // if (_this.options.isSticky || !_this.options.forceTop) { - // setTimeout(function() { - // _this.$element[0].style.transform = ''; - // $(window).off('scroll.zf.offcanvas'); - // }, this.options.transitionTime); - // } - if (this.options.closeOnClick) { - this.$exiter.removeClass('is-visible'); + + // Remove `is-visible` class from overlay. + if (this.options.contentOverlay) { + this.$overlay.removeClass('is-visible'); + } + + // If we have `closeOnClick` and `contentOverlay` add `is-closable` class. + if (this.options.closeOnClick && this.options.contentOverlay) { + this.$overlay.removeClass('is-closable'); } this.$triggers.attr('aria-expanded', 'false'); @@ -313,18 +277,13 @@ class OffCanvas { * @function * @private */ - _handleKeyboard(e) { - Foundation.Keyboard.handleKey(e, 'OffCanvas', { - close: () => { - this.close(); - this.$lastTrigger.focus(); - return true; - }, - handled: () => { - e.stopPropagation(); - e.preventDefault(); - } - }); + _handleKeyboard(event) { + if (event.which !== 27) return; + + event.stopPropagation(); + event.preventDefault(); + this.close(); + this.$lastTrigger.focus(); } /** @@ -334,7 +293,7 @@ class OffCanvas { destroy() { this.close(); this.$element.off('.zf.trigger .zf.offcanvas'); - this.$exiter.off('.zf.offcanvas'); + this.$overlay.off('.zf.offcanvas'); Foundation.unregisterPlugin(this); } @@ -348,6 +307,13 @@ OffCanvas.defaults = { */ closeOnClick: true, + /** + * Adds an overlay on top of `[data-off-canvas-content]`. + * @option + * @example true + */ + contentOverlay: true, + /** * Amount of time in ms the open and close transition requires. If none selected, pulls from body style. * @option @@ -355,6 +321,13 @@ OffCanvas.defaults = { */ transitionTime: 0, + /** + * Type of transition for the offcanvas menu. Options are 'push', 'detached' or 'slide'. + * @option + * @example push + */ + transition: 'push', + /** * Direction the offcanvas opens from. Determines class applied to body. * @option @@ -363,11 +336,11 @@ OffCanvas.defaults = { position: 'left', /** - * Force the page to scroll to top on open. + * Force the page to scroll to top or bottom on open. * @option - * @example true + * @example top */ - forceTop: true, + forceTo: null, /** * Allow the offcanvas to remain open for certain breakpoints. @@ -384,7 +357,7 @@ OffCanvas.defaults = { revealOn: null, /** - * Force focus to the offcanvas on open. If true, will focus the opening trigger on close. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes. + * Force focus to the offcanvas on open. If true, will focus the opening trigger on close. * @option * @example true */ diff --git a/scss/components/_off-canvas.scss b/scss/components/_off-canvas.scss index 6bf5b92f6..2d37baaa1 100644 --- a/scss/components/_off-canvas.scss +++ b/scss/components/_off-canvas.scss @@ -6,43 +6,55 @@ /// @group off-canvas //// -/// Width of an off-canvas menu. +/// Width of a left/right off-canvas panel. /// @type Number $offcanvas-size: 250px !default; -/// Background color of an off-canvas menu. +/// Height of a top/bottom off-canvas panel. +/// @type Number +$offcanvas-vertical-size: 250px !default; + +/// Background color of an off-canvas panel. /// @type Color $offcanvas-background: $light-gray !default; -/// Z-index of an off-canvas menu. +/// Z-index of an off-canvas panel with the `push` transition. +/// @type Number +$offcanvas-push-zindex: 1 !default; + +/// Z-index of an off-canvas panel with the `overlap` transition. /// @type Number -$offcanvas-zindex: -1 !default; +$offcanvas-overlap-zindex: 10 !default; -/// Length of the animation on an off-canvas menu. +/// Z-index of an off-canvas panel using the `reveal-for-*` classes or mixin. +/// @type Number +$offcanvas-reveal-zindex: 1 !default; + +/// Length of the animation on an off-canvas panel. /// @type Number $offcanvas-transition-length: 0.5s !default; -/// Timing function of the animation on an off-canvas menu. +/// Timing function of the animation on an off-canvas panel. /// @type Keyword $offcanvas-transition-timing: ease !default; /// If `true`, a revealed off-canvas will be fixed-position, and scroll with the screen. $offcanvas-fixed-reveal: true !default; -/// Background color for the overlay that appears when an off-canvas menu is open. +/// Background color for the overlay that appears when an off-canvas panel is open. /// @type Color $offcanvas-exit-background: rgba($white, 0.25) !default; -/// Height of a vertical off-canvas menu. -/// @type Number -$offcanvas-vertical-size: 250px !default; - -/// CSS class used for the main content area. The off-canvas mixins use this to target the page body. +/// CSS class used for the main content area. The off-canvas mixins use this to target the page content. $maincontent-class: 'off-canvas-content' !default; -/// Box shadow to place under the main content area. This shadow overlaps the off-canvas menus. +/// Background color of the main content area. +/// @type Color +$maincontent-background: $body-background !default; + +/// Box shadow thats applied to the main content (or to the off-canvas panel if overlap transition). /// @type Shadow -$maincontent-shadow: 0 0 10px rgba($black, 0.5) !default; +$offcanvas-shadow: 0 0 10px rgba($black, 0.5) !default; /// Adds baseline styles for off-canvas. This CSS is required to make the other pieces work. @mixin off-canvas-basics { @@ -52,113 +64,178 @@ $maincontent-shadow: 0 0 10px rgba($black, 0.5) !default; height: 100%; } - .off-canvas-wrapper { - width: 100%; + body { overflow-x: hidden; - overflow-y: hidden; - position: relative; - backface-visibility: hidden; - -webkit-overflow-scrolling: auto; } - .off-canvas-wrapper-inner { - @include clearfix; - position: relative; - width: 100%; - min-height: 100%; - transition: transform $offcanvas-transition-length $offcanvas-transition-timing; - } - - // Container for page content - .off-canvas-content, - .#{$maincontent-class} { - min-height: 100%; - background: $body-background; - transition: transform $offcanvas-transition-length $offcanvas-transition-timing; - backface-visibility: hidden; - z-index: 1; - padding-bottom: 0.1px; // Prevents margin collapsing, which would reveal the box shadow of the wrapper - - @if has-value($maincontent-shadow) { - box-shadow: $maincontent-shadow; - } + // Hides overflow on body when an off-canvas panel is open. + .is-off-canvas-open { + overflow: hidden; } // Click-to-exit overlay (generated by JavaScript) .js-off-canvas-exit { - display: none; - position: absolute; + position: fixed; top: 0; left: 0; + width: 100%; height: 100%; + + transition: opacity $offcanvas-transition-length $offcanvas-transition-timing, visibility $offcanvas-transition-length $offcanvas-transition-timing; + background: $offcanvas-exit-background; - cursor: pointer; - transition: background $offcanvas-transition-length $offcanvas-transition-timing; + + opacity: 0; + visibility: hidden; + + overflow: hidden; + + &.is-visible { + opacity: 1; + visibility: visible; + } + + &.is-closable { + cursor: pointer; + } } } -/// Adds basic styles for an off-canvas menu. -@mixin off-canvas-base { +/// Adds basic styles for an off-canvas panel. +@mixin off-canvas-base( + $background: $offcanvas-background, + $transition: $offcanvas-transition-length $offcanvas-transition-timing +) { @include disable-mouse-outline; - position: absolute; - background: $offcanvas-background; - z-index: $offcanvas-zindex; - max-height: 100%; - overflow-y: auto; - transform: translateX(0) translateY(0); + + position: fixed; + z-index: $offcanvas-push-zindex; + + transition: transform $transition; + backface-visibility: hidden; + will-change: transform; + + background: $background; + + // Overlap only styles. + &.is-transition-overlap { + z-index: $offcanvas-overlap-zindex; + + &.is-open { + box-shadow: $offcanvas-shadow; + } + } + + // Sets transform to 0 to show an off-canvas panel. + &.is-open { + transform: translate(0, 0); + } } +/// Adds styles to position an off-canvas panel to the left/right/top/bottom. @mixin off-canvas-position( $position: left, - $size: $offcanvas-size, - $fixed: false, - $vertical-size: $offcanvas-vertical-size + $orientation: horizontal, + $size: if($orientation == horizontal, $offcanvas-size, $offcanvas-vertical-size) ) { @if $position == left { - left: -$size; top: 0; + left: 0; + width: $size; + height: 100%; + + transform: translateX(-$size); + overflow-y: auto; + + // Sets the open position for the content + &.is-open ~ .#{$maincontent-class} { + transform: translateX($size); + } } @else if $position == right { - right: -$size; top: 0; + right: 0; + width: $size; + height: 100%; + + transform: translateX($size); + overflow-y: auto; + + // Sets the open position for the content + &.is-open ~ .#{$maincontent-class} { + transform: translateX(-$size); + } } @else if $position == top { - top: -$vertical-size; + top: 0; + left: 0; + width: 100%; + height: $size; + + transform: translateY(-$size); + overflow-x: auto; + + // Sets the open position for the content + &.is-open ~ .#{$maincontent-class} { + transform: translateY($size); + } } + @else if $position == bottom { + bottom: 0; + left: 0; - // Generates an open state class that matches the width of the menu - @at-root { - .is-open-#{$position} { - @if $position == left { - transform: translateX($size); - } - @else if $position == right { - transform: translateX(-$size); - } - @else if $position == top { - transform: translateY($vertical-size); - } + width: 100%; + height: $size; + + transform: translateY($size); + overflow-x: auto; + + // Sets the open position for the content + &.is-open ~ .#{$maincontent-class} { + transform: translateY(-$size); } } + + // No transform on overlap transition + &.is-transition-overlap.is-open ~ .#{$maincontent-class} { + transform: none; + } } -/// Adds styles that reveal an off-canvas menu. -/// @param {Keyword} $position [left] - Position of the off-canvas menu being revealed. +/// Sets the styles for the content container. +@mixin off-canvas-content( + $background: $maincontent-background +) { + transition: transform $offcanvas-transition-length $offcanvas-transition-timing; + backface-visibility: hidden; + will-change: transform; + + background: $background; + + .is-off-canvas-open & { + box-shadow: $offcanvas-shadow; + } +} + +/// Adds styles that reveal an off-canvas panel. @mixin off-canvas-reveal( - $position: left +$position: left, +$zindex: $offcanvas-reveal-zindex, +$content: $maincontent-class ) { - #{$position}: 0; - z-index: auto; + transform: none; + z-index: $zindex; + visibility: visible; - @if $offcanvas-fixed-reveal { - position: fixed; + @if not $offcanvas-fixed-reveal { + position: relative; } - & ~ .#{$maincontent-class} { + & ~ .#{$content} { margin-#{$position}: $offcanvas-size; } } @@ -169,13 +246,20 @@ $maincontent-shadow: 0 0 10px rgba($black, 0.5) !default; // Off-canvas container .off-canvas { @include off-canvas-base; + } - &.position-left { @include off-canvas-position(left); } - &.position-right { @include off-canvas-position(right); } - &.position-top { @include off-canvas-position(top); } + // Off-canvas position classes + .position-left { @include off-canvas-position(left, horizontal); } + .position-right { @include off-canvas-position(right, horizontal); } + .position-top { @include off-canvas-position(top, vertical); } + .position-bottom { @include off-canvas-position(bottom, vertical); } + + // Off-canvas content + .off-canvas-content { + @include off-canvas-content; } - // Reveal off-canvas menu on larger screens + // Reveal off-canvas panel on larger screens @TODO @each $name, $value in $breakpoint-classes { @if $name != $-zf-zero-breakpoint { @include breakpoint($name) { @@ -190,7 +274,12 @@ $maincontent-shadow: 0 0 10px rgba($black, 0.5) !default; .position-top.reveal-for-#{$name} { @include off-canvas-reveal(top); } + + .position-bottom.reveal-for-#{$name} { + @include off-canvas-reveal(bottom); + } } } } } +