packages feed

reload-0.0.0.1: web/bower_components/app-layout/app-drawer/app-drawer.html

<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://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 http://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: visibility 0.2s ease;
      }

      :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: 0.2s ease;
        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: opacity 0.2s ease;
        -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 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
        }
      },

      observers: [
        'resetLayout(position, isAttached)',
        '_resetPosition(align, isAttached)'
      ],

      _translateOffset: 0,

      _trackDetails: null,

      _drawerState: 0,

      _boundEscKeydownHandler: null,

      _firstTabStop: null,

      _lastTabStop: null,

      ready: 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._setTransitionDuration('0s');
      },

      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).
        Polymer.RenderStatus.afterNextRender(this, function() {
          this._setTransitionDuration('');
          this._boundEscKeydownHandler = this._escKeydownHandler.bind(this);
          this._resetDrawerState();

          this.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');
        });
      },

      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) {
          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._setTransitionDuration('0s');
        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';
        }

        // Trigger app-drawer-transitioned now since there will be no transitionend event.
        if (isInEndState) {
          this._resetDrawerState();
        }

        this._setTransitionDuration('');
        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._setTransitionDuration((this._FLING_INITIAL_SLOPE * dx / velocity) + 'ms');
        this._setTransitionTimingFunction(this._FLING_TIMING_FUNCTION);

        this._resetDrawerTranslate();
      },

      _transitionend: function(event) {
        // 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.
        var target = Polymer.dom(event).rootTarget;
        if (target === this.$.contentContainer || target === this.$.scrim) {

          // If the drawer was flinging, we need to reset the style attributes.
          if (this._drawerState === this._DRAWER_STATE.FLINGING) {
            this._setTransitionDuration('');
            this._setTransitionTimingFunction('');
            this.style.visibility = '';
          }

          this._resetDrawerState();
        }
      },

      _setTransitionDuration: function(duration) {
        this.$.contentContainer.style.transitionDuration = duration;
        this.$.scrim.style.transitionDuration = duration;
      },

      _setTransitionTimingFunction: 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();
            }
          }
        }
      },

      _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 has changed.
       *
       * @event app-drawer-reset-layout
       */

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