]> git.ipfire.org Git - thirdparty/foundation/foundation-sites.git/commitdiff
Rewrite of off canvas Sass and JavaScript. Initial commit, still needs tidying up.
authorBrett Mason <brettsmason@gmail.com>
Thu, 10 Nov 2016 20:22:00 +0000 (20:22 +0000)
committerBrett Mason <brettsmason@gmail.com>
Thu, 10 Nov 2016 20:22:00 +0000 (20:22 +0000)
js/foundation.offcanvas.js
scss/components/_off-canvas.scss

index b1676253da142ca58e57322b6e67c48fa1f4fde2..9c1286f1d1b4c8367d72d0f53c43846032040a30 100644 (file)
@@ -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
    */
index 6bf5b92f6530b83d8ea91b31db08ead475abbc35..2d37baaa1028d54e641f2ba04be843101596c307 100644 (file)
@@ -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);
+        }
       }
     }
   }
 }
+