<!– @license Copyright © 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at polymer.github.io/LICENSE.txt The complete set of authors may be found at polymer.github.io/AUTHORS.txt The complete set of contributors may be found at polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at polymer.github.io/PATENTS.txt –>

<link rel=“import” href=“../../polymer/polymer.html”> <link rel=“import” href=“../../iron-flex-layout/iron-flex-layout.html”>

<!– app-drawer is a navigation drawer that can slide in from the left or right.

Example:

Align the drawer at the start, which is left in LTR layouts (default):

“`html <app-drawer opened></app-drawer> “`

Align the drawer at the end:

“`html <app-drawer align=“end” opened></app-drawer> “`

To make the contents of the drawer scrollable, create a wrapper for the scroll content, and apply height and overflow styles to it.

“`html <app-drawer>

<div style="height: 100%; overflow: auto;"></div>

</app-drawer> “`

### Styling

Custom property | Description | Default ———————————|—————————————-|——————– `–app-drawer-width` | Width of the drawer | 256px `–app-drawer-content-container` | Mixin for the drawer content container | {} `–app-drawer-scrim-background` | Background for the scrim | rgba(0, 0, 0, 0.5)

@group App Elements @element app-drawer @demo app-drawer/demo/left-drawer.html Simple Left Drawer @demo app-drawer/demo/right-drawer.html Right Drawer with Icons –>

<dom-module id=“app-drawer”>

<template>
  <style>
    :host {
      position: fixed;
      top: -120px;
      right: 0;
      bottom: -120px;
      left: 0;

      visibility: hidden;

      transition-property: visibility;
    }

    :host([opened]) {
      visibility: visible;
    }

    :host([persistent]) {
      width: var(--app-drawer-width, 256px);
    }

    :host([persistent][position=left]) {
      right: auto;
    }

    :host([persistent][position=right]) {
      left: auto;
    }

    #contentContainer {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;

      width: var(--app-drawer-width, 256px);
      padding: 120px 0;

      transition-property: -webkit-transform;
      transition-property: transform;
      -webkit-transform: translate3d(-100%, 0, 0);
      transform: translate3d(-100%, 0, 0);

      background-color: #FFF;

      @apply(--app-drawer-content-container);
    }

    :host([position=right]) > #contentContainer {
      right: 0;
      left: auto;

      -webkit-transform: translate3d(100%, 0, 0);
      transform: translate3d(100%, 0, 0);
    }

    :host([swipe-open]) > #contentContainer::after {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 100%;

      visibility: visible;

      width: 20px;

      content: '';
    }

    :host([swipe-open][position=right]) > #contentContainer::after {
      right: 100%;
      left: auto;
    }

    :host([opened]) > #contentContainer {
      -webkit-transform: translate3d(0, 0, 0);
      transform: translate3d(0, 0, 0);
    }

    #scrim {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;

      transition-property: opacity;
      -webkit-transform: translateZ(0);
      transform:  translateZ(0);

      opacity: 0;
      background: var(--app-drawer-scrim-background, rgba(0, 0, 0, 0.5));
    }

    :host([opened]) > #scrim {
      opacity: 1;
    }

    :host([opened][persistent]) > #scrim {
      visibility: hidden;
      /**
       * NOTE(keanulee): Keep both opacity: 0 and visibility: hidden to prevent the
       * scrim from showing when toggling between closed and opened/persistent.
       */

      opacity: 0;
    }
  </style>

  <div id="scrim" on-tap="close"></div>

  <div id="contentContainer">
    <content></content>
  </div>
</template>

<script>

  Polymer({
    is: 'app-drawer',

    properties: {
      /**
       * The opened state of the drawer.
       */
      opened: {
        type: Boolean,
        value: false,
        notify: true,
        reflectToAttribute: true
      },

      /**
       * The drawer does not have a scrim and cannot be swiped close.
       */
      persistent: {
        type: Boolean,
        value: false,
        reflectToAttribute: true
      },

      /**
       * The transition duration of the drawer in milliseconds.
       */
      transitionDuration: {
        type: Number,
        value: 200
      },

      /**
       * The alignment of the drawer on the screen ('left', 'right', 'start' or 'end').
       * 'start' computes to left and 'end' to right in LTR layout and vice versa in RTL
       * layout.
       */
      align: {
        type: String,
        value: 'left'
      },

      /**
       * The computed, read-only position of the drawer on the screen ('left' or 'right').
       */
      position: {
        type: String,
        readOnly: true,
        reflectToAttribute: true
      },

      /**
       * Create an area at the edge of the screen to swipe open the drawer.
       */
      swipeOpen: {
        type: Boolean,
        value: false,
        reflectToAttribute: true
      },

      /**
       * Trap keyboard focus when the drawer is opened and not persistent.
       */
      noFocusTrap: {
        type: Boolean,
        value: false
      },

      /**
       * Disables swiping on the drawer.
       */
      disableSwipe: {
        type: Boolean,
        value: false
      }
    },

    observers: [
      'resetLayout(position, isAttached)',
      '_resetPosition(align, isAttached)',
      '_styleTransitionDuration(transitionDuration)',
      '_openedPersistentChanged(opened, persistent)'
    ],

    _translateOffset: 0,

    _trackDetails: null,

    _drawerState: 0,

    _boundEscKeydownHandler: null,

    _firstTabStop: null,

    _lastTabStop: null,

    attached: function() {
      // Only transition the drawer after its first render (e.g. app-drawer-layout
      // may need to set the initial opened state which should not be transitioned).
      this._styleTransitionDuration(0);
      Polymer.RenderStatus.afterNextRender(this, function() {
        this._styleTransitionDuration(this.transitionDuration);
        this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
        this._resetDrawerState();

        // contentContainer will transition on opened state changed, and scrim will
        // transition on persistent state changed when opened - these are the
        // transitions we are interested in.
        this.$.scrim.addEventListener('transitionend', this._transitionend.bind(this));
        this.$.contentContainer.addEventListener('transitionend', this._transitionend.bind(this));

        this.addEventListener('keydown', this._tabKeydownHandler.bind(this))

        // Only listen for horizontal track so you can vertically scroll inside the drawer.
        this.listen(this, 'track', '_track');
        this.setScrollDirection('y');
      });

      this.fire('app-drawer-attached');
    },

    detached: function() {
      document.removeEventListener('keydown', this._boundEscKeydownHandler);
    },

    /**
     * Opens the drawer.
     */
    open: function() {
      this.opened = true;
    },

    /**
     * Closes the drawer.
     */
    close: function() {
      this.opened = false;
    },

    /**
     * Toggles the drawer open and close.
     */
    toggle: function() {
      this.opened = !this.opened;
    },

    /**
     * Gets the width of the drawer.
     *
     * @return {number} The width of the drawer in pixels.
     */
    getWidth: function() {
      return this.$.contentContainer.offsetWidth;
    },

    /**
     * Resets the layout. The event fired is used by app-drawer-layout to position the
     * content.
     *
     * @method resetLayout
     */
    resetLayout: function() {
      this.fire('app-drawer-reset-layout');
    },

    _isRTL: function() {
      return window.getComputedStyle(this).direction === 'rtl';
    },

    _resetPosition: function() {
      switch (this.align) {
        case 'start':
          this._setPosition(this._isRTL() ? 'right' : 'left');
          return;
        case 'end':
          this._setPosition(this._isRTL() ? 'left' : 'right');
          return;
      }
      this._setPosition(this.align);
    },

    _escKeydownHandler: function(event) {
      var ESC_KEYCODE = 27;
      if (event.keyCode === ESC_KEYCODE) {
        // Prevent any side effects if app-drawer closes.
        event.preventDefault();
        this.close();
      }
    },

    _track: function(event) {
      if (this.persistent || this.disableSwipe) {
        return;
      }

      // Disable user selection on desktop.
      event.preventDefault();

      switch (event.detail.state) {
        case 'start':
          this._trackStart(event);
          break;
        case 'track':
          this._trackMove(event);
          break;
        case 'end':
          this._trackEnd(event);
          break;
      }
    },

    _trackStart: function(event) {
      this._drawerState = this._DRAWER_STATE.TRACKING;

      // Disable transitions since style attributes will reflect user track events.
      this._styleTransitionDuration(0);
      this.style.visibility = 'visible';

      var rect = this.$.contentContainer.getBoundingClientRect();
      if (this.position === 'left') {
        this._translateOffset = rect.left;
      } else {
        this._translateOffset = rect.right - window.innerWidth;
      }

      this._trackDetails = [];
    },

    _trackMove: function(event) {
      this._translateDrawer(event.detail.dx + this._translateOffset);

      // Use Date.now() since event.timeStamp is inconsistent across browsers (e.g. most
      // browsers use milliseconds but FF 44 uses microseconds).
      this._trackDetails.push({
        dx: event.detail.dx,
        timeStamp: Date.now()
      });
    },

    _trackEnd: function(event) {
      var x = event.detail.dx + this._translateOffset;
      var drawerWidth = this.getWidth();
      var isPositionLeft = this.position === 'left';
      var isInEndState = isPositionLeft ? (x >= 0 || x <= -drawerWidth) :
        (x <= 0 || x >= drawerWidth);

      if (!isInEndState) {
        // No longer need the track events after this method returns - allow them to be GC'd.
        var trackDetails = this._trackDetails;
        this._trackDetails = null;

        this._flingDrawer(event, trackDetails);
        if (this._drawerState === this._DRAWER_STATE.FLINGING) {
          return;
        }
      }

      // If the drawer is not flinging, toggle the opened state based on the position of
      // the drawer.
      var halfWidth = drawerWidth / 2;
      if (event.detail.dx < -halfWidth) {
        this.opened = this.position === 'right';
      } else if (event.detail.dx > halfWidth) {
        this.opened = this.position === 'left';
      }

      if (isInEndState) {
        // Reset drawer state now since there will be no transitionend event.
        this._resetDrawerState();
      }

      this._styleTransitionDuration(this.transitionDuration);
      this._resetDrawerTranslate();
      this.style.visibility = '';
    },

    _calculateVelocity: function(event, trackDetails) {
      // Find the oldest track event that is within 100ms using binary search.
      var now = Date.now();
      var timeLowerBound = now - 100;
      var trackDetail;
      var min = 0;
      var max = trackDetails.length - 1;

      while (min <= max) {
        // Floor of average of min and max.
        var mid = (min + max) >> 1;
        var d = trackDetails[mid];
        if (d.timeStamp >= timeLowerBound) {
          trackDetail = d;
          max = mid - 1;
        } else {
          min = mid + 1;
        }
      }

      if (trackDetail) {
        var dx = event.detail.dx - trackDetail.dx;
        var dt = (now - trackDetail.timeStamp) || 1;
        return dx / dt;
      }
      return 0;
    },

    _flingDrawer: function(event, trackDetails) {
      var velocity = this._calculateVelocity(event, trackDetails);

      // Do not fling if velocity is not above a threshold.
      if (Math.abs(velocity) < this._MIN_FLING_THRESHOLD) {
        return;
      }

      this._drawerState = this._DRAWER_STATE.FLINGING;

      var x = event.detail.dx + this._translateOffset;
      var drawerWidth = this.getWidth();
      var isPositionLeft = this.position === 'left';
      var isVelocityPositive = velocity > 0;
      var isClosingLeft = !isVelocityPositive && isPositionLeft;
      var isClosingRight = isVelocityPositive && !isPositionLeft;
      var dx;
      if (isClosingLeft) {
        dx = -(x + drawerWidth);
      } else if (isClosingRight) {
        dx = (drawerWidth - x);
      } else {
        dx = -x;
      }

      // Enforce a minimum transition velocity to make the drawer feel snappy.
      if (isVelocityPositive) {
        velocity = Math.max(velocity, this._MIN_TRANSITION_VELOCITY);
        this.opened = this.position === 'left';
      } else {
        velocity = Math.min(velocity, -this._MIN_TRANSITION_VELOCITY);
        this.opened = this.position === 'right';
      }

      // Calculate the amount of time needed to finish the transition based on the
      // initial slope of the timing function.
      this._styleTransitionDuration(this._FLING_INITIAL_SLOPE * dx / velocity);
      this._styleTransitionTimingFunction(this._FLING_TIMING_FUNCTION);

      this._resetDrawerTranslate();
    },

    _transitionend: function() {
      // If the drawer was flinging, we need to reset the style attributes.
      if (this._drawerState === this._DRAWER_STATE.FLINGING) {
        this._styleTransitionDuration(this.transitionDuration);
        this._styleTransitionTimingFunction('');
        this.style.visibility = '';
      }

      this._resetDrawerState();
    },

    _styleTransitionDuration: function(duration) {
      this.style.transitionDuration = duration + 'ms';
      this.$.contentContainer.style.transitionDuration = duration + 'ms';
      this.$.scrim.style.transitionDuration = duration + 'ms';
    },

    _styleTransitionTimingFunction: function(timingFunction) {
      this.$.contentContainer.style.transitionTimingFunction = timingFunction;
      this.$.scrim.style.transitionTimingFunction = timingFunction;
    },

    _translateDrawer: function(x) {
      var drawerWidth = this.getWidth();

      if (this.position === 'left') {
        x = Math.max(-drawerWidth, Math.min(x, 0));
        this.$.scrim.style.opacity = 1 + x / drawerWidth;
      } else {
        x = Math.max(0, Math.min(x, drawerWidth));
        this.$.scrim.style.opacity = 1 - x / drawerWidth;
      }

      this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
    },

    _resetDrawerTranslate: function() {
      this.$.scrim.style.opacity = '';
      this.transform('', this.$.contentContainer);
    },

    _resetDrawerState: function() {
      var oldState = this._drawerState;
      if (this.opened) {
        this._drawerState = this.persistent ?
          this._DRAWER_STATE.OPENED_PERSISTENT : this._DRAWER_STATE.OPENED;
      } else {
        this._drawerState = this._DRAWER_STATE.CLOSED;
      }

      if (oldState !== this._drawerState) {
        if (this._drawerState === this._DRAWER_STATE.OPENED) {
          this._setKeyboardFocusTrap();
          document.addEventListener('keydown', this._boundEscKeydownHandler);
          document.body.style.overflow = 'hidden';
        } else {
          document.removeEventListener('keydown', this._boundEscKeydownHandler);
          document.body.style.overflow = '';
        }

        // Don't fire the event on initial load.
        if (oldState !== this._DRAWER_STATE.INIT) {
          this.fire('app-drawer-transitioned');
        }
      }
    },

    _setKeyboardFocusTrap: function() {
      if (this.noFocusTrap) {
        return;
      }

      // NOTE: Unless we use /deep/ (which we shouldn't since it's deprecated), this will
      // not select focusable elements inside shadow roots.
      var focusableElementsSelector = [
          'a[href]:not([tabindex="-1"])',
          'area[href]:not([tabindex="-1"])',
          'input:not([disabled]):not([tabindex="-1"])',
          'select:not([disabled]):not([tabindex="-1"])',
          'textarea:not([disabled]):not([tabindex="-1"])',
          'button:not([disabled]):not([tabindex="-1"])',
          'iframe:not([tabindex="-1"])',
          '[tabindex]:not([tabindex="-1"])',
          '[contentEditable=true]:not([tabindex="-1"])'
        ].join(',');
      var focusableElements = Polymer.dom(this).querySelectorAll(focusableElementsSelector);

      if (focusableElements.length > 0) {
        this._firstTabStop = focusableElements[0];
        this._lastTabStop = focusableElements[focusableElements.length - 1];
      } else {
        // Reset saved tab stops when there are no focusable elements in the drawer.
        this._firstTabStop = null;
        this._lastTabStop = null;
      }

      // Focus on app-drawer if it has non-zero tabindex. Otherwise, focus the first focusable
      // element in the drawer, if it exists. Use the tabindex attribute since the this.tabIndex
      // property in IE/Edge returns 0 (instead of -1) when the attribute is not set.
      var tabindex = this.getAttribute('tabindex');
      if (tabindex && parseInt(tabindex, 10) > -1) {
        this.focus();
      } else if (this._firstTabStop) {
        this._firstTabStop.focus();
      }
    },

    _tabKeydownHandler: function(event) {
      if (this.noFocusTrap) {
        return;
      }

      var TAB_KEYCODE = 9;
      if (this._drawerState === this._DRAWER_STATE.OPENED && event.keyCode === TAB_KEYCODE) {
        if (event.shiftKey) {
          if (this._firstTabStop && Polymer.dom(event).localTarget === this._firstTabStop) {
            event.preventDefault();
            this._lastTabStop.focus();
          }
        } else {
          if (this._lastTabStop && Polymer.dom(event).localTarget === this._lastTabStop) {
            event.preventDefault();
            this._firstTabStop.focus();
          }
        }
      }
    },

    _openedPersistentChanged: function() {
      if (this.transitionDuration === 0) {
        // Reset drawer state now since there will be no transitionend event.
        this._resetDrawerState();
      }
    },

    _MIN_FLING_THRESHOLD: 0.2,

    _MIN_TRANSITION_VELOCITY: 1.2,

    _FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',

    _FLING_INITIAL_SLOPE: 1.5,

    _DRAWER_STATE: {
      INIT: 0,
      OPENED: 1,
      OPENED_PERSISTENT: 2,
      CLOSED: 3,
      TRACKING: 4,
      FLINGING: 5
    }

    /**
     * Fired when the layout of app-drawer is attached.
     *
     * @event app-drawer-attached
     */

    /**
     * Fired when the layout of app-drawer has changed.
     *
     * @event app-drawer-reset-layout
     */

    /**
     * Fired when app-drawer has finished transitioning.
     *
     * @event app-drawer-transitioned
     */
  });
</script>

</dom-module>