UNPKG

api-console-assets

Version:

This repo only exists to publish api console components to npm

682 lines (561 loc) 20.1 kB
<!-- @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-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.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) { this.debounce('_resetDrawerState', this._resetDrawerState); } else { this.debounce('_resetDrawerState', this._resetDrawerState, this.transitionDuration); } 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. var t = this._FLING_INITIAL_SLOPE * dx / velocity this._styleTransitionDuration(t); this._styleTransitionTimingFunction(this._FLING_TIMING_FUNCTION); this._resetDrawerTranslate(); this.debounce('_resetDrawerState', this._resetDrawerState, t); }, _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 the drawer was flinging, we need to reset the style attributes. if (oldState === this._DRAWER_STATE.FLINGING) { this._styleTransitionDuration(this.transitionDuration); this._styleTransitionTimingFunction(''); this.style.visibility = ''; } 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() { // Use a debounce timer instead of transitionend since transitionend won't fire when // app-drawer is display: none. this.debounce('_resetDrawerState', this._resetDrawerState, this.transitionDuration); }, _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>