this._init();
this._events();
- Foundation.registerPlugin(this, 'OffCanvas')
- Foundation.Keyboard.register('OffCanvas', {
- 'ESCAPE': 'close'
- });
-
+ Foundation.registerPlugin(this, '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+'"]')
.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);
}
}
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;
}
}
'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)});
}
}
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)
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) {
}
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();
}
}
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
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');
* @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();
}
/**
destroy() {
this.close();
this.$element.off('.zf.trigger .zf.offcanvas');
- this.$exiter.off('.zf.offcanvas');
+ this.$overlay.off('.zf.offcanvas');
Foundation.unregisterPlugin(this);
}
*/
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
*/
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
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.
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
*/
/// @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 {
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;
}
}
// 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) {
.position-top.reveal-for-#{$name} {
@include off-canvas-reveal(top);
}
+
+ .position-bottom.reveal-for-#{$name} {
+ @include off-canvas-reveal(bottom);
+ }
}
}
}
}
+