diff options
author | dbeam <dbeam@chromium.org> | 2016-02-09 12:29:16 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-02-09 20:30:46 +0000 |
commit | 82518f51af928f359dc51011a56ae221b9ff9508 (patch) | |
tree | c53b45d2c4402ad4fbf0f376fab680550e72c336 | |
parent | 27a62f7974d680eb378bc23ef961eddcc19875b6 (diff) | |
download | chromium_src-82518f51af928f359dc51011a56ae221b9ff9508.zip chromium_src-82518f51af928f359dc51011a56ae221b9ff9508.tar.gz chromium_src-82518f51af928f359dc51011a56ae221b9ff9508.tar.bz2 |
Unrestrict version of PolymerElements/iron-list and update it
Also pulls in whatever happened in the last few day in Polymer
R=dpapad@chromium.org
BUG=none
NOPRESUBMIT=true # crisper.js
Review URL: https://codereview.chromium.org/1681053002
Cr-Commit-Position: refs/heads/master@{#374455}
16 files changed, 1490 insertions, 832 deletions
diff --git a/chrome/browser/resources/md_downloads/crisper.js b/chrome/browser/resources/md_downloads/crisper.js index 1c61c9f..0c4f256 100644 --- a/chrome/browser/resources/md_downloads/crisper.js +++ b/chrome/browser/resources/md_downloads/crisper.js @@ -9,6 +9,9 @@ */ var global = this; +/** @typedef {{eventName: string, uid: number}} */ +var WebUIListener; + /** Platform, package, object property, and Event support. **/ var cr = function() { 'use strict'; @@ -357,11 +360,13 @@ var cr = function() { } /** - * A registry of callbacks keyed by event name. Used by addWebUIListener to - * register listeners. - * @type {!Object<Array<Function>>} + * A map of maps associating event names with listeners. The 2nd level map + * associates a listener ID with the callback function, such that individual + * listeners can be removed from an event without affecting other listeners of + * the same event. + * @type {!Object<!Object<!Function>>} */ - var webUIListenerMap = Object.create(null); + var webUIListenerMap = {}; /** * The named method the WebUI handler calls directly when an event occurs. @@ -369,26 +374,52 @@ var cr = function() { * of the JS invocation; additionally, the handler may supply any number of * other arguments that will be forwarded to the listener callbacks. * @param {string} event The name of the event that has occurred. + * @param {...*} var_args Additional arguments passed from C++. */ - function webUIListenerCallback(event) { - var listenerCallbacks = webUIListenerMap[event]; - for (var i = 0; i < listenerCallbacks.length; i++) { - var callback = listenerCallbacks[i]; - callback.apply(null, Array.prototype.slice.call(arguments, 1)); + function webUIListenerCallback(event, var_args) { + var eventListenersMap = webUIListenerMap[event]; + if (!eventListenersMap) { + // C++ event sent for an event that has no listeners. + // TODO(dpapad): Should a warning be displayed here? + return; + } + + var args = Array.prototype.slice.call(arguments, 1); + for (var listenerId in eventListenersMap) { + eventListenersMap[listenerId].apply(null, args); } } /** * Registers a listener for an event fired from WebUI handlers. Any number of * listeners may register for a single event. - * @param {string} event The event to listen to. - * @param {Function} callback The callback run when the event is fired. + * @param {string} eventName The event to listen to. + * @param {!Function} callback The callback run when the event is fired. + * @return {!WebUIListener} An object to be used for removing a listener via + * cr.removeWebUIListener. Should be treated as read-only. */ - function addWebUIListener(event, callback) { - if (event in webUIListenerMap) - webUIListenerMap[event].push(callback); - else - webUIListenerMap[event] = [callback]; + function addWebUIListener(eventName, callback) { + webUIListenerMap[eventName] = webUIListenerMap[eventName] || {}; + var uid = createUid(); + webUIListenerMap[eventName][uid] = callback; + return {eventName: eventName, uid: uid}; + } + + /** + * Removes a listener. Does nothing if the specified listener is not found. + * @param {!WebUIListener} listener The listener to be removed (as returned by + * addWebUIListener). + * @return {boolean} Whether the given listener was found and actually + * removed. + */ + function removeWebUIListener(listener) { + var listenerExists = webUIListenerMap[listener.eventName] && + webUIListenerMap[listener.eventName][listener.uid]; + if (listenerExists) { + delete webUIListenerMap[listener.eventName][listener.uid]; + return true; + } + return false; } return { @@ -401,11 +432,14 @@ var cr = function() { exportPath: exportPath, getUid: getUid, makePublic: makePublic, - webUIResponse: webUIResponse, + PropertyKind: PropertyKind, + + // C++ <-> JS communication related methods. + addWebUIListener: addWebUIListener, + removeWebUIListener: removeWebUIListener, sendWithPromise: sendWithPromise, webUIListenerCallback: webUIListenerCallback, - addWebUIListener: addWebUIListener, - PropertyKind: PropertyKind, + webUIResponse: webUIResponse, get doc() { return document; @@ -430,6 +464,11 @@ var cr = function() { get isLinux() { return /Linux/.test(navigator.userAgent); }, + + /** Whether this is on Android. */ + get isAndroid() { + return /Android/.test(navigator.userAgent); + } }; }(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. @@ -987,9 +1026,19 @@ cr.define('cr.ui', function() { */ function $(id) { var el = document.getElementById(id); - var message = - 'Element ' + el + ' with id "' + id + '" is not an HTMLElement.'; - return el ? assertInstanceof(el, HTMLElement, message) : null; + return el ? assertInstanceof(el, HTMLElement) : null; +} + +// TODO(devlin): This should return SVGElement, but closure compiler is missing +// those externs. +/** + * Alias for document.getElementById. Found elements must be SVGElements. + * @param {string} id The ID of the element to find. + * @return {Element} The found element or null if not found. + */ +function getSVGElement(id) { + var el = document.getElementById(id); + return el ? assertInstanceof(el, Element) : null; } /** @@ -1505,8 +1554,12 @@ function assertNotReached(opt_message) { * @template T */ function assertInstanceof(value, type, opt_message) { - assert(value instanceof type, - opt_message || value + ' is not a[n] ' + (type.name || typeof type)); + // We don't use assert immediately here so that we avoid constructing an error + // message if we don't have to. + if (!(value instanceof type)) { + assertNotReached(opt_message || 'Value ' + value + + ' is not a[n] ' + (type.name || typeof type)); + } return value; }; // Copyright 2015 The Chromium Authors. All rights reserved. @@ -1979,11 +2032,654 @@ i18nTemplate.process(document, loadTimeData); } }; (function() { + 'use strict'; + + /** + * Chrome uses an older version of DOM Level 3 Keyboard Events + * + * Most keys are labeled as text, but some are Unicode codepoints. + * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set + */ + var KEY_IDENTIFIER = { + 'U+0008': 'backspace', + 'U+0009': 'tab', + 'U+001B': 'esc', + 'U+0020': 'space', + 'U+007F': 'del' + }; + + /** + * Special table for KeyboardEvent.keyCode. + * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better + * than that. + * + * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode + */ + var KEY_CODE = { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + 27: 'esc', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 32: 'space', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 46: 'del', + 106: '*' + }; + + /** + * MODIFIER_KEYS maps the short name for modifier keys used in a key + * combo string to the property name that references those same keys + * in a KeyboardEvent instance. + */ + var MODIFIER_KEYS = { + 'shift': 'shiftKey', + 'ctrl': 'ctrlKey', + 'alt': 'altKey', + 'meta': 'metaKey' + }; + + /** + * KeyboardEvent.key is mostly represented by printable character made by + * the keyboard, with unprintable keys labeled nicely. + * + * However, on OS X, Alt+char can make a Unicode character that follows an + * Apple-specific mapping. In this case, we fall back to .keyCode. + */ + var KEY_CHAR = /[a-z0-9*]/; + + /** + * Matches a keyIdentifier string. + */ + var IDENT_CHAR = /U\+/; + + /** + * Matches arrow keys in Gecko 27.0+ + */ + var ARROW_KEY = /^arrow/; + + /** + * Matches space keys everywhere (notably including IE10's exceptional name + * `spacebar`). + */ + var SPACE_KEY = /^space(bar)?/; + + /** + * Transforms the key. + * @param {string} key The KeyBoardEvent.key + * @param {Boolean} [noSpecialChars] Limits the transformation to + * alpha-numeric characters. + */ + function transformKey(key, noSpecialChars) { + var validKey = ''; + if (key) { + var lKey = key.toLowerCase(); + if (lKey === ' ' || SPACE_KEY.test(lKey)) { + validKey = 'space'; + } else if (lKey.length == 1) { + if (!noSpecialChars || KEY_CHAR.test(lKey)) { + validKey = lKey; + } + } else if (ARROW_KEY.test(lKey)) { + validKey = lKey.replace('arrow', ''); + } else if (lKey == 'multiply') { + // numpad '*' can map to Multiply on IE/Windows + validKey = '*'; + } else { + validKey = lKey; + } + } + return validKey; + } + + function transformKeyIdentifier(keyIdent) { + var validKey = ''; + if (keyIdent) { + if (keyIdent in KEY_IDENTIFIER) { + validKey = KEY_IDENTIFIER[keyIdent]; + } else if (IDENT_CHAR.test(keyIdent)) { + keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); + validKey = String.fromCharCode(keyIdent).toLowerCase(); + } else { + validKey = keyIdent.toLowerCase(); + } + } + return validKey; + } + + function transformKeyCode(keyCode) { + var validKey = ''; + if (Number(keyCode)) { + if (keyCode >= 65 && keyCode <= 90) { + // ascii a-z + // lowercase is 32 offset from uppercase + validKey = String.fromCharCode(32 + keyCode); + } else if (keyCode >= 112 && keyCode <= 123) { + // function keys f1-f12 + validKey = 'f' + (keyCode - 112); + } else if (keyCode >= 48 && keyCode <= 57) { + // top 0-9 keys + validKey = String(48 - keyCode); + } else if (keyCode >= 96 && keyCode <= 105) { + // num pad 0-9 + validKey = String(96 - keyCode); + } else { + validKey = KEY_CODE[keyCode]; + } + } + return validKey; + } + + /** + * Calculates the normalized key for a KeyboardEvent. + * @param {KeyboardEvent} keyEvent + * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key + * transformation to alpha-numeric chars. This is useful with key + * combinations like shift + 2, which on FF for MacOS produces + * keyEvent.key = @ + * To get 2 returned, set noSpecialChars = true + * To get @ returned, set noSpecialChars = false + */ + function normalizedKeyForEvent(keyEvent, noSpecialChars) { + // Fall back from .key, to .keyIdentifier, to .keyCode, and then to + // .detail.key to support artificial keyboard events. + return transformKey(keyEvent.key, noSpecialChars) || + transformKeyIdentifier(keyEvent.keyIdentifier) || + transformKeyCode(keyEvent.keyCode) || + transformKey(keyEvent.detail.key, noSpecialChars) || ''; + } + + function keyComboMatchesEvent(keyCombo, event) { + // For combos with modifiers we support only alpha-numeric keys + var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); + return keyEvent === keyCombo.key && + (!keyCombo.hasModifiers || ( + !!event.shiftKey === !!keyCombo.shiftKey && + !!event.ctrlKey === !!keyCombo.ctrlKey && + !!event.altKey === !!keyCombo.altKey && + !!event.metaKey === !!keyCombo.metaKey) + ); + } + + function parseKeyComboString(keyComboString) { + if (keyComboString.length === 1) { + return { + combo: keyComboString, + key: keyComboString, + event: 'keydown' + }; + } + return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) { + var eventParts = keyComboPart.split(':'); + var keyName = eventParts[0]; + var event = eventParts[1]; + + if (keyName in MODIFIER_KEYS) { + parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; + parsedKeyCombo.hasModifiers = true; + } else { + parsedKeyCombo.key = keyName; + parsedKeyCombo.event = event || 'keydown'; + } + + return parsedKeyCombo; + }, { + combo: keyComboString.split(':').shift() + }); + } + + function parseEventString(eventString) { + return eventString.trim().split(' ').map(function(keyComboString) { + return parseKeyComboString(keyComboString); + }); + } + + /** + * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing + * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). + * The element takes care of browser differences with respect to Keyboard events + * and uses an expressive syntax to filter key presses. + * + * Use the `keyBindings` prototype property to express what combination of keys + * will trigger the event to fire. + * + * Use the `key-event-target` attribute to set up event handlers on a specific + * node. + * The `keys-pressed` event will fire when one of the key combinations set with the + * `keys` property is pressed. + * + * @demo demo/index.html + * @polymerBehavior + */ + Polymer.IronA11yKeysBehavior = { + properties: { + /** + * The HTMLElement that will be firing relevant KeyboardEvents. + */ + keyEventTarget: { + type: Object, + value: function() { + return this; + } + }, + + /** + * If true, this property will cause the implementing element to + * automatically stop propagation on any handled KeyboardEvents. + */ + stopKeyboardEventPropagation: { + type: Boolean, + value: false + }, + + _boundKeyHandlers: { + type: Array, + value: function() { + return []; + } + }, + + // We use this due to a limitation in IE10 where instances will have + // own properties of everything on the "prototype". + _imperativeKeyBindings: { + type: Object, + value: function() { + return {}; + } + } + }, + + observers: [ + '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' + ], + + keyBindings: {}, + + registered: function() { + this._prepKeyBindings(); + }, + + attached: function() { + this._listenKeyEventListeners(); + }, + + detached: function() { + this._unlistenKeyEventListeners(); + }, + + /** + * Can be used to imperatively add a key binding to the implementing + * element. This is the imperative equivalent of declaring a keybinding + * in the `keyBindings` prototype property. + */ + addOwnKeyBinding: function(eventString, handlerName) { + this._imperativeKeyBindings[eventString] = handlerName; + this._prepKeyBindings(); + this._resetKeyEventListeners(); + }, + + /** + * When called, will remove all imperatively-added key bindings. + */ + removeOwnKeyBindings: function() { + this._imperativeKeyBindings = {}; + this._prepKeyBindings(); + this._resetKeyEventListeners(); + }, + + keyboardEventMatchesKeys: function(event, eventString) { + var keyCombos = parseEventString(eventString); + for (var i = 0; i < keyCombos.length; ++i) { + if (keyComboMatchesEvent(keyCombos[i], event)) { + return true; + } + } + return false; + }, + + _collectKeyBindings: function() { + var keyBindings = this.behaviors.map(function(behavior) { + return behavior.keyBindings; + }); + + if (keyBindings.indexOf(this.keyBindings) === -1) { + keyBindings.push(this.keyBindings); + } + + return keyBindings; + }, + + _prepKeyBindings: function() { + this._keyBindings = {}; + + this._collectKeyBindings().forEach(function(keyBindings) { + for (var eventString in keyBindings) { + this._addKeyBinding(eventString, keyBindings[eventString]); + } + }, this); + + for (var eventString in this._imperativeKeyBindings) { + this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]); + } + + // Give precedence to combos with modifiers to be checked first. + for (var eventName in this._keyBindings) { + this._keyBindings[eventName].sort(function (kb1, kb2) { + var b1 = kb1[0].hasModifiers; + var b2 = kb2[0].hasModifiers; + return (b1 === b2) ? 0 : b1 ? -1 : 1; + }) + } + }, + + _addKeyBinding: function(eventString, handlerName) { + parseEventString(eventString).forEach(function(keyCombo) { + this._keyBindings[keyCombo.event] = + this._keyBindings[keyCombo.event] || []; + + this._keyBindings[keyCombo.event].push([ + keyCombo, + handlerName + ]); + }, this); + }, + + _resetKeyEventListeners: function() { + this._unlistenKeyEventListeners(); + + if (this.isAttached) { + this._listenKeyEventListeners(); + } + }, + + _listenKeyEventListeners: function() { + Object.keys(this._keyBindings).forEach(function(eventName) { + var keyBindings = this._keyBindings[eventName]; + var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); + + this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]); + + this.keyEventTarget.addEventListener(eventName, boundKeyHandler); + }, this); + }, + + _unlistenKeyEventListeners: function() { + var keyHandlerTuple; + var keyEventTarget; + var eventName; + var boundKeyHandler; + + while (this._boundKeyHandlers.length) { + // My kingdom for block-scope binding and destructuring assignment.. + keyHandlerTuple = this._boundKeyHandlers.pop(); + keyEventTarget = keyHandlerTuple[0]; + eventName = keyHandlerTuple[1]; + boundKeyHandler = keyHandlerTuple[2]; + + keyEventTarget.removeEventListener(eventName, boundKeyHandler); + } + }, + + _onKeyBindingEvent: function(keyBindings, event) { + if (this.stopKeyboardEventPropagation) { + event.stopPropagation(); + } + + // if event has been already prevented, don't do anything + if (event.defaultPrevented) { + return; + } + + for (var i = 0; i < keyBindings.length; i++) { + var keyCombo = keyBindings[i][0]; + var handlerName = keyBindings[i][1]; + if (keyComboMatchesEvent(keyCombo, event)) { + this._triggerKeyHandler(keyCombo, handlerName, event); + // exit the loop if eventDefault was prevented + if (event.defaultPrevented) { + return; + } + } + } + }, + + _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { + var detail = Object.create(keyCombo); + detail.keyboardEvent = keyboardEvent; + var event = new CustomEvent(keyCombo.event, { + detail: detail, + cancelable: true + }); + this[handlerName].call(this, event); + if (event.defaultPrevented) { + keyboardEvent.preventDefault(); + } + } + }; + })(); +/** + * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll events from a + * designated scroll target. + * + * Elements that consume this behavior can override the `_scrollHandler` + * method to add logic on the scroll event. + * + * @demo demo/scrolling-region.html Scrolling Region + * @demo demo/document.html Document Element + * @polymerBehavior + */ + Polymer.IronScrollTargetBehavior = { + + properties: { + + /** + * Specifies the element that will handle the scroll event + * on the behalf of the current element. This is typically a reference to an `Element`, + * but there are a few more posibilities: + * + * ### Elements id + * + *```html + * <div id="scrollable-element" style="overflow-y: auto;"> + * <x-element scroll-target="scrollable-element"> + * Content + * </x-element> + * </div> + *``` + * In this case, `scrollTarget` will point to the outer div element. Alternatively, + * you can set the property programatically: + * + *```js + * appHeader.scrollTarget = document.querySelector('#scrollable-element'); + *``` + * + * @type {HTMLElement} + */ + scrollTarget: { + type: HTMLElement, + value: function() { + return this._defaultScrollTarget; + } + } + }, + + observers: [ + '_scrollTargetChanged(scrollTarget, isAttached)' + ], + + _scrollTargetChanged: function(scrollTarget, isAttached) { + // Remove lister to the current scroll target + if (this._oldScrollTarget) { + if (this._oldScrollTarget === this._doc) { + window.removeEventListener('scroll', this._boundScrollHandler); + } else if (this._oldScrollTarget.removeEventListener) { + this._oldScrollTarget.removeEventListener('scroll', this._boundScrollHandler); + } + this._oldScrollTarget = null; + } + if (isAttached) { + // Support element id references + if (typeof scrollTarget === 'string') { + + var ownerRoot = Polymer.dom(this).getOwnerRoot(); + this.scrollTarget = (ownerRoot && ownerRoot.$) ? + ownerRoot.$[scrollTarget] : Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget); + + } else if (this._scrollHandler) { + + this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this); + // Add a new listener + if (scrollTarget === this._doc) { + window.addEventListener('scroll', this._boundScrollHandler); + if (this._scrollTop !== 0 || this._scrollLeft !== 0) { + this._scrollHandler(); + } + } else if (scrollTarget && scrollTarget.addEventListener) { + scrollTarget.addEventListener('scroll', this._boundScrollHandler); + } + this._oldScrollTarget = scrollTarget; + } + } + }, + + /** + * Runs on every scroll event. Consumer of this behavior may want to override this method. + * + * @protected + */ + _scrollHandler: function scrollHandler() {}, + + /** + * The default scroll target. Consumers of this behavior may want to customize + * the default scroll target. + * + * @type {Element} + */ + get _defaultScrollTarget() { + return this._doc; + }, + + /** + * Shortcut for the document element + * + * @type {Element} + */ + get _doc() { + return this.ownerDocument.documentElement; + }, + + /** + * Gets the number of pixels that the content of an element is scrolled upward. + * + * @type {number} + */ + get _scrollTop() { + if (this._isValidScrollTarget()) { + return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop; + } + return 0; + }, + + /** + * Gets the number of pixels that the content of an element is scrolled to the left. + * + * @type {number} + */ + get _scrollLeft() { + if (this._isValidScrollTarget()) { + return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft; + } + return 0; + }, + + /** + * Sets the number of pixels that the content of an element is scrolled upward. + * + * @type {number} + */ + set _scrollTop(top) { + if (this.scrollTarget === this._doc) { + window.scrollTo(window.pageXOffset, top); + } else if (this._isValidScrollTarget()) { + this.scrollTarget.scrollTop = top; + } + }, + + /** + * Sets the number of pixels that the content of an element is scrolled to the left. + * + * @type {number} + */ + set _scrollLeft(left) { + if (this.scrollTarget === this._doc) { + window.scrollTo(left, window.pageYOffset); + } else if (this._isValidScrollTarget()) { + this.scrollTarget.scrollLeft = left; + } + }, + + /** + * Scrolls the content to a particular place. + * + * @method scroll + * @param {number} top The top position + * @param {number} left The left position + */ + scroll: function(top, left) { + if (this.scrollTarget === this._doc) { + window.scrollTo(top, left); + } else if (this._isValidScrollTarget()) { + this.scrollTarget.scrollTop = top; + this.scrollTarget.scrollLeft = left; + } + }, + + /** + * Gets the width of the scroll target. + * + * @type {number} + */ + get _scrollTargetWidth() { + if (this._isValidScrollTarget()) { + return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth; + } + return 0; + }, + + /** + * Gets the height of the scroll target. + * + * @type {number} + */ + get _scrollTargetHeight() { + if (this._isValidScrollTarget()) { + return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight; + } + return 0; + }, + + /** + * Returns true if the scroll target is a valid HTMLElement. + * + * @return {boolean} + */ + _isValidScrollTarget: function() { + return this.scrollTarget instanceof HTMLElement; + } + }; +(function() { var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; var DEFAULT_PHYSICAL_COUNT = 3; var MAX_PHYSICAL_COUNT = 500; + var HIDDEN_Y = '-10000px'; Polymer({ @@ -2069,18 +2765,27 @@ i18nTemplate.process(document, loadTimeData); observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEnabled)', - '_multiSelectionChanged(multiSelection)' + '_multiSelectionChanged(multiSelection)', + '_setOverflow(scrollTarget)' ], behaviors: [ Polymer.Templatizer, - Polymer.IronResizableBehavior + Polymer.IronResizableBehavior, + Polymer.IronA11yKeysBehavior, + Polymer.IronScrollTargetBehavior ], listeners: { 'iron-resize': '_resizeHandler' }, + keyBindings: { + 'up': '_didMoveUp', + 'down': '_didMoveDown', + 'enter': '_didEnter' + }, + /** * The ratio of hidden tiles that should remain in the scroll direction. * Recommended value ~0.5, so it will distribute tiles evely in both directions. @@ -2088,12 +2793,6 @@ i18nTemplate.process(document, loadTimeData); _ratio: 0.5, /** - * The element that controls the scroll - * @type {?Element} - */ - _scroller: null, - - /** * The padding-top value of the `scroller` element */ _scrollerPaddingTop: 0, @@ -2124,7 +2823,7 @@ i18nTemplate.process(document, loadTimeData); _physicalSize: 0, /** - * The average `offsetHeight` of the tiles observed till now. + * The average `F` of the tiles observed till now. */ _physicalAverage: 0, @@ -2182,13 +2881,21 @@ i18nTemplate.process(document, loadTimeData); _physicalSizes: null, /** - * A cached value for the visible index. + * A cached value for the first visible index. * See `firstVisibleIndex` * @type {?number} */ _firstVisibleIndexVal: null, /** + * A cached value for the last visible index. + * See `lastVisibleIndex` + * @type {?number} + */ + _lastVisibleIndexVal: null, + + + /** * A Polymer collection for the items. * @type {?Polymer.Collection} */ @@ -2211,6 +2918,23 @@ i18nTemplate.process(document, loadTimeData); _maxPages: 3, /** + * The currently focused item index. + */ + _focusedIndex: 0, + + /** + * The the item that is focused if it is moved offscreen. + * @private {?TemplatizerNode} + */ + _offscreenFocusedItem: null, + + /** + * The item that backfills the `_offscreenFocusedItem` in the physical items + * list when that item is moved offscreen. + */ + _focusBackfillItem: null, + + /** * The bottom of the physical content. */ get _physicalBottom() { @@ -2228,7 +2952,7 @@ i18nTemplate.process(document, loadTimeData); * The n-th item rendered in the last physical item. */ get _virtualEnd() { - return this._virtualStartVal + this._physicalCount - 1; + return this._virtualStart + this._physicalCount - 1; }, /** @@ -2263,8 +2987,13 @@ i18nTemplate.process(document, loadTimeData); set _virtualStart(val) { // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val)); - this._physicalStart = this._virtualStartVal % this._physicalCount; - this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount; + if (this._physicalCount === 0) { + this._physicalStart = 0; + this._physicalEnd = 0; + } else { + this._physicalStart = this._virtualStartVal % this._physicalCount; + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount; + } }, /** @@ -2289,7 +3018,7 @@ i18nTemplate.process(document, loadTimeData); * True if the current list is visible. */ get _isVisible() { - return this._scroller && Boolean(this._scroller.offsetWidth || this._scroller.offsetHeight); + return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight); }, /** @@ -2298,10 +3027,8 @@ i18nTemplate.process(document, loadTimeData); * @type {number} */ get firstVisibleIndex() { - var physicalOffset; - if (this._firstVisibleIndexVal === null) { - physicalOffset = this._physicalTop; + var physicalOffset = this._physicalTop; this._firstVisibleIndexVal = this._iterateItems( function(pidx, vidx) { @@ -2312,54 +3039,52 @@ i18nTemplate.process(document, loadTimeData); } }) || 0; } - return this._firstVisibleIndexVal; }, - ready: function() { - if (IOS_TOUCH_SCROLLING) { - this._scrollListener = function() { - requestAnimationFrame(this._scrollHandler.bind(this)); - }.bind(this); - } else { - this._scrollListener = this._scrollHandler.bind(this); - } - }, - /** - * When the element has been attached to the DOM tree. + * Gets the index of the last visible item in the viewport. + * + * @type {number} */ - attached: function() { - // delegate to the parent's scroller - // e.g. paper-scroll-header-panel - var el = Polymer.dom(this); + get lastVisibleIndex() { + if (this._lastVisibleIndexVal === null) { + var physicalOffset = this._physicalTop; - var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode); - if (parentNode && parentNode.scroller) { - this._scroller = parentNode.scroller; - } else { - this._scroller = this; - this.classList.add('has-scroller'); - } + this._iterateItems(function(pidx, vidx) { + physicalOffset += this._physicalSizes[pidx]; - if (IOS_TOUCH_SCROLLING) { - this._scroller.style.webkitOverflowScrolling = 'touch'; + if(physicalOffset <= this._scrollBottom) { + this._lastVisibleIndexVal = vidx; + } + }); } + return this._lastVisibleIndexVal; + }, - this._scroller.addEventListener('scroll', this._scrollListener); + ready: function() { + this.addEventListener('focus', this._didFocus.bind(this), true); + }, + attached: function() { this.updateViewportBoundaries(); this._render(); }, - /** - * When the element has been removed from the DOM tree. - */ detached: function() { this._itemsRendered = false; - if (this._scroller) { - this._scroller.removeEventListener('scroll', this._scrollListener); - } + }, + + get _defaultScrollTarget() { + return this; + }, + + /** + * Set the overflow property if this element has its own scrolling region + */ + _setOverflow: function(scrollTarget) { + this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; + this.style.overflow = scrollTarget === this ? 'auto' : ''; }, /** @@ -2369,20 +3094,20 @@ i18nTemplate.process(document, loadTimeData); * @method updateViewportBoundaries */ updateViewportBoundaries: function() { - var scrollerStyle = window.getComputedStyle(this._scroller); + var scrollerStyle = window.getComputedStyle(this.scrollTarget); this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); - this._viewportSize = this._scroller.offsetHeight; + this._viewportSize = this._scrollTargetHeight; }, /** * Update the models, the position of the * items in the viewport and recycle tiles as needed. */ - _refresh: function() { + _scrollHandler: function() { // clamp the `scrollTop` value // IE 10|11 scrollTop may go above `_maxScrollTop` // iOS `scrollTop` may go below 0 and above `_maxScrollTop` - var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.scrollTop)); + var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)); var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom; var ratio = this._ratio; var delta = scrollTop - this._scrollPosition; @@ -2396,6 +3121,7 @@ i18nTemplate.process(document, loadTimeData); // clear cached visible index this._firstVisibleIndexVal = null; + this._lastVisibleIndexVal = null; scrollBottom = this._scrollBottom; physicalBottom = this._physicalBottom; @@ -2485,17 +3211,21 @@ i18nTemplate.process(document, loadTimeData); }, /** - * Update the list of items, starting from the `_virtualStartVal` item. + * Update the list of items, starting from the `_virtualStart` item. * @param {!Array<number>=} itemSet * @param {!Array<number>=} movingUp */ _update: function(itemSet, movingUp) { + // manage focus + if (this._isIndexRendered(this._focusedIndex)) { + this._restoreFocusedItem(); + } else { + this._createFocusBackfillItem(); + } // update models this._assignModels(itemSet); - // measure heights this._updateMetrics(itemSet); - // adjust offset after measuring if (movingUp) { while (movingUp.length) { @@ -2504,10 +3234,8 @@ i18nTemplate.process(document, loadTimeData); } // update the position of the items this._positionItems(); - // set the scroller size this._updateScrollerSize(); - // increase the pool of physical items this._increasePoolIfNeeded(); }, @@ -2527,7 +3255,6 @@ i18nTemplate.process(document, loadTimeData); physicalItems[i] = inst.root.querySelector('*'); Polymer.dom(this).appendChild(inst.root); } - return physicalItems; }, @@ -2537,24 +3264,24 @@ i18nTemplate.process(document, loadTimeData); * if the physical size is shorter than `_optPhysicalSize` */ _increasePoolIfNeeded: function() { - if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize) { - // 0 <= `currentPage` <= `_maxPages` - var currentPage = Math.floor(this._physicalSize / this._viewportSize); - - if (currentPage === 0) { - // fill the first page - this.async(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5))); - } else if (this._lastPage !== currentPage) { - // once a page is filled up, paint it and defer the next increase - requestAnimationFrame(this._increasePool.bind(this, 1)); - } else { - // fill the rest of the pages - this.async(this._increasePool.bind(this, 1)); - } - this._lastPage = currentPage; - return true; + if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) { + return false; } - return false; + // 0 <= `currentPage` <= `_maxPages` + var currentPage = Math.floor(this._physicalSize / this._viewportSize); + if (currentPage === 0) { + // fill the first page + this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5))); + } else if (this._lastPage !== currentPage) { + // paint the page and defer the next increase + // wait 16ms which is rough enough to get paint cycle. + Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16)); + } else { + // fill the rest of the pages + this._debounceTemplate(this._increasePool.bind(this, 1)); + } + this._lastPage = currentPage; + return true; }, /** @@ -2564,7 +3291,7 @@ i18nTemplate.process(document, loadTimeData); // limit the size var nextPhysicalCount = Math.min( this._physicalCount + missingItems, - this._virtualCount, + this._virtualCount - this._virtualStart, MAX_PHYSICAL_COUNT ); var prevPhysicalCount = this._physicalCount; @@ -2590,6 +3317,7 @@ i18nTemplate.process(document, loadTimeData); if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) { this._lastPage = 0; this._update(); + this._scrollHandler(); this._itemsRendered = true; } }, @@ -2601,11 +3329,11 @@ i18nTemplate.process(document, loadTimeData); if (!this.ctor) { // Template instance props that should be excluded from forwarding var props = {}; - props.__key__ = true; props[this.as] = true; props[this.indexAs] = true; props[this.selectedAs] = true; + props.tabIndex = true; this._instanceProps = props; this._userTemplate = Polymer.dom(this).querySelector('template'); @@ -2673,6 +3401,10 @@ i18nTemplate.process(document, loadTimeData); var key = path.substring(0, dot < 0 ? path.length : dot); var idx = this._physicalIndexForKey[key]; var row = this._physicalItems[idx]; + + if (idx === this._focusedIndex && this._offscreenFocusedItem) { + row = this._offscreenFocusedItem; + } if (row) { var inst = row._templateInstance; if (dot >= 0) { @@ -2691,17 +3423,18 @@ i18nTemplate.process(document, loadTimeData); */ _itemsChanged: function(change) { if (change.path === 'items') { + + this._restoreFocusedItem(); // render the new set this._itemsRendered = false; - // update the whole set - this._virtualStartVal = 0; + this._virtualStart = 0; this._physicalTop = 0; this._virtualCount = this.items ? this.items.length : 0; + this._focusedIndex = 0; this._collection = this.items ? Polymer.Collection.get(this.items) : null; this._physicalIndexForKey = {}; - // scroll to the top this._resetScrollPosition(0); // create the initial physical items @@ -2710,17 +3443,20 @@ i18nTemplate.process(document, loadTimeData); this._physicalItems = this._createPool(this._physicalCount); this._physicalSizes = new Array(this._physicalCount); } - - this.debounce('refresh', this._render); + this._debounceTemplate(this._render); } else if (change.path === 'items.splices') { // render the new set this._itemsRendered = false; - this._adjustVirtualIndex(change.value.indexSplices); this._virtualCount = this.items ? this.items.length : 0; - this.debounce('refresh', this._render); + this._debounceTemplate(this._render); + + if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) { + this._focusedIndex = 0; + } + this._debounceTemplate(this._render); } else { // update a single item @@ -2742,19 +3478,15 @@ i18nTemplate.process(document, loadTimeData); idx = splice.index; // We only need to care about changes happening above the current position - if (idx >= this._virtualStartVal) { + if (idx >= this._virtualStart) { break; } this._virtualStart = this._virtualStart + - Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStartVal); + Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart); } }, - _scrollHandler: function() { - this._refresh(); - }, - /** * Executes a provided function per every physical index in `itemSet` * `itemSet` default value is equivalent to the entire set of physical indexes. @@ -2769,9 +3501,9 @@ i18nTemplate.process(document, loadTimeData); for (i = 0; i < itemSet.length; i++) { pidx = itemSet[i]; if (pidx >= this._physicalStart) { - vidx = this._virtualStartVal + (pidx - this._physicalStart); + vidx = this._virtualStart + (pidx - this._physicalStart); } else { - vidx = this._virtualStartVal + (this._physicalCount - this._physicalStart) + pidx; + vidx = this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; } if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; @@ -2779,17 +3511,14 @@ i18nTemplate.process(document, loadTimeData); } } else { pidx = this._physicalStart; - vidx = this._virtualStartVal; + vidx = this._virtualStart; for (; pidx < this._physicalCount; pidx++, vidx++) { if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; } } - - pidx = 0; - - for (; pidx < this._physicalStart; pidx++, vidx++) { + for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; } @@ -2807,12 +3536,12 @@ i18nTemplate.process(document, loadTimeData); var inst = el._templateInstance; var item = this.items && this.items[vidx]; - if (item) { + if (item !== undefined && item !== null) { inst[this.as] = item; inst.__key__ = this._collection.getKey(item); - inst[this.selectedAs] = - /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item); + inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item); inst[this.indexAs] = vidx; + inst.tabIndex = vidx === this._focusedIndex ? 0 : -1; el.removeAttribute('hidden'); this._physicalIndexForKey[inst.__key__] = pidx; } else { @@ -2829,23 +3558,26 @@ i18nTemplate.process(document, loadTimeData); * @param {!Array<number>=} itemSet */ _updateMetrics: function(itemSet) { + // Make sure we distributed all the physical items + // so we can measure them + Polymer.dom.flush(); + var newPhysicalSize = 0; var oldPhysicalSize = 0; var prevAvgCount = this._physicalAverageCount; var prevPhysicalAvg = this._physicalAverage; - // Make sure we distributed all the physical items - // so we can measure them - Polymer.dom.flush(); this._iterateItems(function(pidx, vidx) { + oldPhysicalSize += this._physicalSizes[pidx] || 0; this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; newPhysicalSize += this._physicalSizes[pidx]; this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; + }, itemSet); this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize; - this._viewportSize = this._scroller.offsetHeight; + this._viewportSize = this._scrollTargetHeight; // update the average if we measured something if (this._physicalAverageCount !== prevAvgCount) { @@ -2865,7 +3597,7 @@ i18nTemplate.process(document, loadTimeData); this._iterateItems(function(pidx) { - this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pidx]); + this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); y += this._physicalSizes[pidx]; }); @@ -2875,15 +3607,14 @@ i18nTemplate.process(document, loadTimeData); * Adjusts the scroll position when it was overestimated. */ _adjustScrollPosition: function() { - var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop : + var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(this._scrollPosition + this._physicalTop, 0); if (deltaHeight) { this._physicalTop = this._physicalTop - deltaHeight; - // juking scroll position during interial scrolling on iOS is no bueno if (!IOS_TOUCH_SCROLLING) { - this._resetScrollPosition(this._scroller.scrollTop - deltaHeight); + this._resetScrollPosition(this._scrollTop - deltaHeight); } } }, @@ -2892,9 +3623,9 @@ i18nTemplate.process(document, loadTimeData); * Sets the position of the scroll. */ _resetScrollPosition: function(pos) { - if (this._scroller) { - this._scroller.scrollTop = pos; - this._scrollPosition = this._scroller.scrollTop; + if (this.scrollTarget) { + this._scrollTop = pos; + this._scrollPosition = this._scrollTop; } }, @@ -2905,7 +3636,7 @@ i18nTemplate.process(document, loadTimeData); */ _updateScrollerSize: function(forceUpdate) { this._estScrollHeight = (this._physicalBottom + - Math.max(this._virtualCount - this._physicalCount - this._virtualStartVal, 0) * this._physicalAverage); + Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage); forceUpdate = forceUpdate || this._scrollHeight === 0; forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; @@ -2929,20 +3660,18 @@ i18nTemplate.process(document, loadTimeData); return; } - var firstVisible = this.firstVisibleIndex; + Polymer.dom.flush(); + var firstVisible = this.firstVisibleIndex; idx = Math.min(Math.max(idx, 0), this._virtualCount-1); // start at the previous virtual item // so we have a item above the first visible item this._virtualStart = idx - 1; - // assign new models this._assignModels(); - // measure the new sizes this._updateMetrics(); - // estimate new physical offset this._physicalTop = this._virtualStart * this._physicalAverage; @@ -2957,21 +3686,17 @@ i18nTemplate.process(document, loadTimeData); currentTopItem = (currentTopItem + 1) % this._physicalCount; currentVirtualItem++; } - // update the scroller size this._updateScrollerSize(true); - // update the position of the items this._positionItems(); - // set the new scroll position - this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); - + this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1); // increase the pool of physical items if needed this._increasePoolIfNeeded(); - // clear cached visible index this._firstVisibleIndexVal = null; + this._lastVisibleIndexVal = null; }, /** @@ -2987,7 +3712,11 @@ i18nTemplate.process(document, loadTimeData); * when the element is resized. */ _resizeHandler: function() { - this.debounce('resize', function() { + // iOS fires the resize event when the address bar slides up + if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) { + return; + } + this._debounceTemplate(function() { this._render(); if (this._itemsRendered && this._physicalItems && this._isVisible) { this._resetAverage(); @@ -3013,12 +3742,14 @@ i18nTemplate.process(document, loadTimeData); * @param {(Object|number)} item The item object or its index */ _getNormalizedItem: function(item) { - if (typeof item === 'number') { - item = this.items[item]; - if (!item) { - throw new RangeError('<item> not found'); + if (this._collection.getKey(item) === undefined) { + if (typeof item === 'number') { + item = this.items[item]; + if (!item) { + throw new RangeError('<item> not found'); + } + return item; } - } else if (this._collection.getKey(item) === undefined) { throw new TypeError('<item> should be a valid item'); } return item; @@ -3041,6 +3772,7 @@ i18nTemplate.process(document, loadTimeData); model[this.selectedAs] = true; } this.$.selector.select(item); + this.updateSizeForItem(item); }, /** @@ -3058,6 +3790,7 @@ i18nTemplate.process(document, loadTimeData); model[this.selectedAs] = false; } this.$.selector.deselect(item); + this.updateSizeForItem(item); }, /** @@ -3103,20 +3836,15 @@ i18nTemplate.process(document, loadTimeData); * it will remove the listener otherwise. */ _selectionEnabledChanged: function(selectionEnabled) { - if (selectionEnabled) { - this.listen(this, 'tap', '_selectionHandler'); - this.listen(this, 'keypress', '_selectionHandler'); - } else { - this.unlisten(this, 'tap', '_selectionHandler'); - this.unlisten(this, 'keypress', '_selectionHandler'); - } + var handler = selectionEnabled ? this.listen : this.unlisten; + handler.call(this, this, 'tap', '_selectionHandler'); }, /** * Select an item from an event object. */ _selectionHandler: function(e) { - if (e.type !== 'keypress' || e.keyCode === 13) { + if (this.selectionEnabled) { var model = this.modelForElement(e.target); if (model) { this.toggleSelectionForItem(model[this.as]); @@ -3144,6 +3872,135 @@ i18nTemplate.process(document, loadTimeData); this._updateMetrics([pidx]); this._positionItems(); } + }, + + _isIndexRendered: function(idx) { + return idx >= this._virtualStart && idx <= this._virtualEnd; + }, + + _getPhysicalItemForIndex: function(idx, force) { + if (!this._collection) { + return null; + } + if (!this._isIndexRendered(idx)) { + if (force) { + this.scrollToIndex(idx); + return this._getPhysicalItemForIndex(idx, false); + } + return null; + } + var item = this._getNormalizedItem(idx); + var physicalItem = this._physicalItems[this._physicalIndexForKey[this._collection.getKey(item)]]; + + return physicalItem || null; + }, + + _focusPhysicalItem: function(idx) { + this._restoreFocusedItem(); + + var physicalItem = this._getPhysicalItemForIndex(idx, true); + if (!physicalItem) { + return; + } + var SECRET = ~(Math.random() * 100); + var model = physicalItem._templateInstance; + var focusable; + + model.tabIndex = SECRET; + // the focusable element could be the entire physical item + if (physicalItem.tabIndex === SECRET) { + focusable = physicalItem; + } + // the focusable element could be somewhere within the physical item + if (!focusable) { + focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]'); + } + // restore the tab index + model.tabIndex = 0; + focusable && focusable.focus(); + }, + + _restoreFocusedItem: function() { + if (!this._offscreenFocusedItem) { + return; + } + var item = this._getNormalizedItem(this._focusedIndex); + var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; + + if (pidx !== undefined) { + this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]); + this._physicalItems[pidx] = this._offscreenFocusedItem; + } + this._offscreenFocusedItem = null; + }, + + _removeFocusedItem: function() { + if (!this._offscreenFocusedItem) { + return; + } + Polymer.dom(this).removeChild(this._offscreenFocusedItem); + this._offscreenFocusedItem = null; + this._focusBackfillItem = null; + }, + + _createFocusBackfillItem: function() { + if (this._offscreenFocusedItem) { + return; + } + var item = this._getNormalizedItem(this._focusedIndex); + var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; + + this._offscreenFocusedItem = this._physicalItems[pidx]; + this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); + + if (!this._focusBackfillItem) { + var stampedTemplate = this.stamp(null); + this._focusBackfillItem = stampedTemplate.root.querySelector('*'); + Polymer.dom(this).appendChild(stampedTemplate.root); + } + this._physicalItems[pidx] = this._focusBackfillItem; + }, + + _didFocus: function(e) { + var targetModel = this.modelForElement(e.target); + var fidx = this._focusedIndex; + + if (!targetModel) { + return; + } + this._restoreFocusedItem(); + + if (this.modelForElement(this._offscreenFocusedItem) === targetModel) { + this.scrollToIndex(fidx); + } else { + // restore tabIndex for the currently focused item + this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1; + // set the tabIndex for the next focused item + targetModel.tabIndex = 0; + fidx = /** @type {{index: number}} */(targetModel).index; + this._focusedIndex = fidx; + // bring the item into view + if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) { + this.scrollToIndex(fidx); + } else { + this._update(); + } + } + }, + + _didMoveUp: function() { + this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1)); + }, + + _didMoveDown: function() { + this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1)); + }, + + _didEnter: function(e) { + // focus the currently focused physical item + this._focusPhysicalItem(this._focusedIndex); + // toggle selection + this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent); } }); @@ -3690,438 +4547,6 @@ Polymer({ } }); -(function() { - 'use strict'; - - /** - * Chrome uses an older version of DOM Level 3 Keyboard Events - * - * Most keys are labeled as text, but some are Unicode codepoints. - * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set - */ - var KEY_IDENTIFIER = { - 'U+0008': 'backspace', - 'U+0009': 'tab', - 'U+001B': 'esc', - 'U+0020': 'space', - 'U+007F': 'del' - }; - - /** - * Special table for KeyboardEvent.keyCode. - * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better - * than that. - * - * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode - */ - var KEY_CODE = { - 8: 'backspace', - 9: 'tab', - 13: 'enter', - 27: 'esc', - 33: 'pageup', - 34: 'pagedown', - 35: 'end', - 36: 'home', - 32: 'space', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 46: 'del', - 106: '*' - }; - - /** - * MODIFIER_KEYS maps the short name for modifier keys used in a key - * combo string to the property name that references those same keys - * in a KeyboardEvent instance. - */ - var MODIFIER_KEYS = { - 'shift': 'shiftKey', - 'ctrl': 'ctrlKey', - 'alt': 'altKey', - 'meta': 'metaKey' - }; - - /** - * KeyboardEvent.key is mostly represented by printable character made by - * the keyboard, with unprintable keys labeled nicely. - * - * However, on OS X, Alt+char can make a Unicode character that follows an - * Apple-specific mapping. In this case, we fall back to .keyCode. - */ - var KEY_CHAR = /[a-z0-9*]/; - - /** - * Matches a keyIdentifier string. - */ - var IDENT_CHAR = /U\+/; - - /** - * Matches arrow keys in Gecko 27.0+ - */ - var ARROW_KEY = /^arrow/; - - /** - * Matches space keys everywhere (notably including IE10's exceptional name - * `spacebar`). - */ - var SPACE_KEY = /^space(bar)?/; - - /** - * Transforms the key. - * @param {string} key The KeyBoardEvent.key - * @param {Boolean} [noSpecialChars] Limits the transformation to - * alpha-numeric characters. - */ - function transformKey(key, noSpecialChars) { - var validKey = ''; - if (key) { - var lKey = key.toLowerCase(); - if (lKey === ' ' || SPACE_KEY.test(lKey)) { - validKey = 'space'; - } else if (lKey.length == 1) { - if (!noSpecialChars || KEY_CHAR.test(lKey)) { - validKey = lKey; - } - } else if (ARROW_KEY.test(lKey)) { - validKey = lKey.replace('arrow', ''); - } else if (lKey == 'multiply') { - // numpad '*' can map to Multiply on IE/Windows - validKey = '*'; - } else { - validKey = lKey; - } - } - return validKey; - } - - function transformKeyIdentifier(keyIdent) { - var validKey = ''; - if (keyIdent) { - if (keyIdent in KEY_IDENTIFIER) { - validKey = KEY_IDENTIFIER[keyIdent]; - } else if (IDENT_CHAR.test(keyIdent)) { - keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16); - validKey = String.fromCharCode(keyIdent).toLowerCase(); - } else { - validKey = keyIdent.toLowerCase(); - } - } - return validKey; - } - - function transformKeyCode(keyCode) { - var validKey = ''; - if (Number(keyCode)) { - if (keyCode >= 65 && keyCode <= 90) { - // ascii a-z - // lowercase is 32 offset from uppercase - validKey = String.fromCharCode(32 + keyCode); - } else if (keyCode >= 112 && keyCode <= 123) { - // function keys f1-f12 - validKey = 'f' + (keyCode - 112); - } else if (keyCode >= 48 && keyCode <= 57) { - // top 0-9 keys - validKey = String(48 - keyCode); - } else if (keyCode >= 96 && keyCode <= 105) { - // num pad 0-9 - validKey = String(96 - keyCode); - } else { - validKey = KEY_CODE[keyCode]; - } - } - return validKey; - } - - /** - * Calculates the normalized key for a KeyboardEvent. - * @param {KeyboardEvent} keyEvent - * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key - * transformation to alpha-numeric chars. This is useful with key - * combinations like shift + 2, which on FF for MacOS produces - * keyEvent.key = @ - * To get 2 returned, set noSpecialChars = true - * To get @ returned, set noSpecialChars = false - */ - function normalizedKeyForEvent(keyEvent, noSpecialChars) { - // Fall back from .key, to .keyIdentifier, to .keyCode, and then to - // .detail.key to support artificial keyboard events. - return transformKey(keyEvent.key, noSpecialChars) || - transformKeyIdentifier(keyEvent.keyIdentifier) || - transformKeyCode(keyEvent.keyCode) || - transformKey(keyEvent.detail.key, noSpecialChars) || ''; - } - - function keyComboMatchesEvent(keyCombo, event) { - // For combos with modifiers we support only alpha-numeric keys - var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers); - return keyEvent === keyCombo.key && - (!keyCombo.hasModifiers || ( - !!event.shiftKey === !!keyCombo.shiftKey && - !!event.ctrlKey === !!keyCombo.ctrlKey && - !!event.altKey === !!keyCombo.altKey && - !!event.metaKey === !!keyCombo.metaKey) - ); - } - - function parseKeyComboString(keyComboString) { - if (keyComboString.length === 1) { - return { - combo: keyComboString, - key: keyComboString, - event: 'keydown' - }; - } - return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) { - var eventParts = keyComboPart.split(':'); - var keyName = eventParts[0]; - var event = eventParts[1]; - - if (keyName in MODIFIER_KEYS) { - parsedKeyCombo[MODIFIER_KEYS[keyName]] = true; - parsedKeyCombo.hasModifiers = true; - } else { - parsedKeyCombo.key = keyName; - parsedKeyCombo.event = event || 'keydown'; - } - - return parsedKeyCombo; - }, { - combo: keyComboString.split(':').shift() - }); - } - - function parseEventString(eventString) { - return eventString.trim().split(' ').map(function(keyComboString) { - return parseKeyComboString(keyComboString); - }); - } - - /** - * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing - * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). - * The element takes care of browser differences with respect to Keyboard events - * and uses an expressive syntax to filter key presses. - * - * Use the `keyBindings` prototype property to express what combination of keys - * will trigger the event to fire. - * - * Use the `key-event-target` attribute to set up event handlers on a specific - * node. - * The `keys-pressed` event will fire when one of the key combinations set with the - * `keys` property is pressed. - * - * @demo demo/index.html - * @polymerBehavior - */ - Polymer.IronA11yKeysBehavior = { - properties: { - /** - * The HTMLElement that will be firing relevant KeyboardEvents. - */ - keyEventTarget: { - type: Object, - value: function() { - return this; - } - }, - - /** - * If true, this property will cause the implementing element to - * automatically stop propagation on any handled KeyboardEvents. - */ - stopKeyboardEventPropagation: { - type: Boolean, - value: false - }, - - _boundKeyHandlers: { - type: Array, - value: function() { - return []; - } - }, - - // We use this due to a limitation in IE10 where instances will have - // own properties of everything on the "prototype". - _imperativeKeyBindings: { - type: Object, - value: function() { - return {}; - } - } - }, - - observers: [ - '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)' - ], - - keyBindings: {}, - - registered: function() { - this._prepKeyBindings(); - }, - - attached: function() { - this._listenKeyEventListeners(); - }, - - detached: function() { - this._unlistenKeyEventListeners(); - }, - - /** - * Can be used to imperatively add a key binding to the implementing - * element. This is the imperative equivalent of declaring a keybinding - * in the `keyBindings` prototype property. - */ - addOwnKeyBinding: function(eventString, handlerName) { - this._imperativeKeyBindings[eventString] = handlerName; - this._prepKeyBindings(); - this._resetKeyEventListeners(); - }, - - /** - * When called, will remove all imperatively-added key bindings. - */ - removeOwnKeyBindings: function() { - this._imperativeKeyBindings = {}; - this._prepKeyBindings(); - this._resetKeyEventListeners(); - }, - - keyboardEventMatchesKeys: function(event, eventString) { - var keyCombos = parseEventString(eventString); - for (var i = 0; i < keyCombos.length; ++i) { - if (keyComboMatchesEvent(keyCombos[i], event)) { - return true; - } - } - return false; - }, - - _collectKeyBindings: function() { - var keyBindings = this.behaviors.map(function(behavior) { - return behavior.keyBindings; - }); - - if (keyBindings.indexOf(this.keyBindings) === -1) { - keyBindings.push(this.keyBindings); - } - - return keyBindings; - }, - - _prepKeyBindings: function() { - this._keyBindings = {}; - - this._collectKeyBindings().forEach(function(keyBindings) { - for (var eventString in keyBindings) { - this._addKeyBinding(eventString, keyBindings[eventString]); - } - }, this); - - for (var eventString in this._imperativeKeyBindings) { - this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]); - } - - // Give precedence to combos with modifiers to be checked first. - for (var eventName in this._keyBindings) { - this._keyBindings[eventName].sort(function (kb1, kb2) { - var b1 = kb1[0].hasModifiers; - var b2 = kb2[0].hasModifiers; - return (b1 === b2) ? 0 : b1 ? -1 : 1; - }) - } - }, - - _addKeyBinding: function(eventString, handlerName) { - parseEventString(eventString).forEach(function(keyCombo) { - this._keyBindings[keyCombo.event] = - this._keyBindings[keyCombo.event] || []; - - this._keyBindings[keyCombo.event].push([ - keyCombo, - handlerName - ]); - }, this); - }, - - _resetKeyEventListeners: function() { - this._unlistenKeyEventListeners(); - - if (this.isAttached) { - this._listenKeyEventListeners(); - } - }, - - _listenKeyEventListeners: function() { - Object.keys(this._keyBindings).forEach(function(eventName) { - var keyBindings = this._keyBindings[eventName]; - var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings); - - this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]); - - this.keyEventTarget.addEventListener(eventName, boundKeyHandler); - }, this); - }, - - _unlistenKeyEventListeners: function() { - var keyHandlerTuple; - var keyEventTarget; - var eventName; - var boundKeyHandler; - - while (this._boundKeyHandlers.length) { - // My kingdom for block-scope binding and destructuring assignment.. - keyHandlerTuple = this._boundKeyHandlers.pop(); - keyEventTarget = keyHandlerTuple[0]; - eventName = keyHandlerTuple[1]; - boundKeyHandler = keyHandlerTuple[2]; - - keyEventTarget.removeEventListener(eventName, boundKeyHandler); - } - }, - - _onKeyBindingEvent: function(keyBindings, event) { - if (this.stopKeyboardEventPropagation) { - event.stopPropagation(); - } - - // if event has been already prevented, don't do anything - if (event.defaultPrevented) { - return; - } - - for (var i = 0; i < keyBindings.length; i++) { - var keyCombo = keyBindings[i][0]; - var handlerName = keyBindings[i][1]; - if (keyComboMatchesEvent(keyCombo, event)) { - this._triggerKeyHandler(keyCombo, handlerName, event); - // exit the loop if eventDefault was prevented - if (event.defaultPrevented) { - return; - } - } - } - }, - - _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) { - var detail = Object.create(keyCombo); - detail.keyboardEvent = keyboardEvent; - var event = new CustomEvent(keyCombo.event, { - detail: detail, - cancelable: true - }); - this[handlerName].call(this, event); - if (event.defaultPrevented) { - keyboardEvent.preventDefault(); - } - } - }; - })(); /** * @demo demo/index.html * @polymerBehavior diff --git a/chrome/browser/resources/md_downloads/vulcanized.html b/chrome/browser/resources/md_downloads/vulcanized.html index 85123ff..1aa9e4e 100644 --- a/chrome/browser/resources/md_downloads/vulcanized.html +++ b/chrome/browser/resources/md_downloads/vulcanized.html @@ -8,6 +8,14 @@ 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 --><!-- @license +Copyright (c) 2016 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 +--><!-- +@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 @@ -1032,14 +1040,13 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN <style> :host { display: block; + position: relative; } - :host(.has-scroller) { - overflow: auto; - } - - :host(:not(.has-scroller)) { - position: relative; + @media only screen and (-webkit-max-device-pixel-ratio: 1) { + :host { + will-change: transform; + } } #items { diff --git a/chrome/browser/resources/md_history/compiled_resources.gyp b/chrome/browser/resources/md_history/compiled_resources.gyp index 463c350..c23dccf 100644 --- a/chrome/browser/resources/md_history/compiled_resources.gyp +++ b/chrome/browser/resources/md_history/compiled_resources.gyp @@ -10,8 +10,10 @@ 'variables': { 'depends': [ + '../../../../third_party/polymer/v1_0/components-chromium/iron-a11y-keys-behavior/iron-a11y-keys-behavior-extracted.js', '../../../../third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js', '../../../../third_party/polymer/v1_0/components-chromium/iron-resizable-behavior/iron-resizable-behavior-extracted.js', + '../../../../third_party/polymer/v1_0/components-chromium/iron-scroll-target-behavior/iron-scroll-target-behavior-extracted.js', '../../../../ui/webui/resources/js/compiled_resources.gyp:load_time_data', '../../../../ui/webui/resources/js/cr.js', '../../../../ui/webui/resources/js/cr/ui/position_util.js', diff --git a/chrome/test/data/webui/md_history/md_history_browsertest.js b/chrome/test/data/webui/md_history/md_history_browsertest.js index 1c4ea3e..bb6c676 100644 --- a/chrome/test/data/webui/md_history/md_history_browsertest.js +++ b/chrome/test/data/webui/md_history/md_history_browsertest.js @@ -22,6 +22,9 @@ MaterialHistoryBrowserTest.prototype = { commandLineSwitches: [{switchName: 'enable-md-history'}], + /** @override */ + runAccessibilityChecks: false, + extraLibraries: PolymerTest.getLibraries(ROOT_PATH).concat([ 'test_util.js', 'history_card_manager_test.js', diff --git a/third_party/polymer/v1_0/bower.json b/third_party/polymer/v1_0/bower.json index 4329d2f..771cc74 100644 --- a/third_party/polymer/v1_0/bower.json +++ b/third_party/polymer/v1_0/bower.json @@ -20,7 +20,7 @@ "iron-iconset-svg": "PolymerElements/iron-iconset-svg#^1.0.0", "iron-image": "PolymerElements/iron-image#^1.0.0", "iron-input": "PolymerElements/iron-input#^1.0.0", - "iron-list": "PolymerElements/iron-list#1.1.7", + "iron-list": "PolymerElements/iron-list#^1.0.0", "iron-media-query": "PolymerElements/iron-media-query#^1.0.0", "iron-menu-behavior": "PolymerElements/iron-menu-behavior#^1.0.0", "iron-meta": "PolymerElements/iron-meta#^1.0.0", diff --git a/third_party/polymer/v1_0/components-chromium/iron-list/.bower.json b/third_party/polymer/v1_0/components-chromium/iron-list/.bower.json index 841812c..6340b3c 100644 --- a/third_party/polymer/v1_0/components-chromium/iron-list/.bower.json +++ b/third_party/polymer/v1_0/components-chromium/iron-list/.bower.json @@ -7,7 +7,7 @@ "list", "virtual-list" ], - "version": "1.1.7", + "version": "1.2.3", "homepage": "https://github.com/PolymerElements/iron-list", "authors": [ "The Polymer Authors" @@ -21,34 +21,36 @@ "ignore": [], "dependencies": { "polymer": "Polymer/polymer#^1.1.0", - "iron-resizable-behavior": "polymerelements/iron-resizable-behavior#^1.0.0" + "iron-resizable-behavior": "polymerelements/iron-resizable-behavior#^1.0.0", + "iron-a11y-keys-behavior": "polymerelements/iron-a11y-keys-behavior#^1.0.0", + "iron-scroll-target-behavior": "PolymerElements/iron-scroll-target-behavior#^1.0.0" }, "devDependencies": { + "app-layout": "PolymerLabs/app-layout#master", "iron-flex-layout": "polymerelements/iron-flex-layout#^1.0.0", "iron-component-page": "polymerelements/iron-component-page#^1.0.0", - "paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0", + "iron-ajax": "polymerelements/iron-ajax#^1.0.0", + "iron-icon": "polymerelements/iron-icon#^1.0.0", + "iron-icons": "polymerelements/iron-icons#^1.0.0", + "iron-scroll-threshold": "polymerelements/iron-scroll-threshold#^1.0.0", "paper-menu": "polymerelements/paper-menu#^1.0.0", "paper-item": "polymerelements/paper-item#^1.0.0", "paper-icon-button": "polymerelements/paper-icon-button#^1.0.0", "paper-button": "polymerelements/paper-button#^1.0.0", - "iron-ajax": "polymerelements/iron-ajax#^1.0.0", - "iron-icon": "polymerelements/iron-icon#^1.0.0", - "iron-icons": "polymerelements/iron-icons#^1.0.0", "paper-badge": "polymerelements/paper-badge#^1.0.0", - "paper-toolbar": "polymerelements/paper-toolbar#^1.0.0", "paper-spinner": "polymerelements/paper-spinner#^1.0.0", "test-fixture": "polymerelements/test-fixture#^1.0.0", "iron-test-helpers": "polymerelements/iron-test-helpers#^1.0.0", "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.2", - "web-component-tester": "polymer/web-component-tester#^3.4.0" + "web-component-tester": "^4.0.0" }, - "_release": "1.1.7", + "_release": "1.2.3", "_resolution": { "type": "version", - "tag": "v1.1.7", - "commit": "a00edace6cac13c16ba6e7a930d29a14b98e44c7" + "tag": "v1.2.3", + "commit": "07957dc10616341606ae030d78bb41492fb7c0cc" }, "_source": "git://github.com/PolymerElements/iron-list.git", - "_target": "1.1.7", + "_target": "^1.0.0", "_originalSource": "PolymerElements/iron-list" }
\ No newline at end of file diff --git a/third_party/polymer/v1_0/components-chromium/iron-list/README.md b/third_party/polymer/v1_0/components-chromium/iron-list/README.md index 1b31769..45dba70 100644 --- a/third_party/polymer/v1_0/components-chromium/iron-list/README.md +++ b/third_party/polymer/v1_0/components-chromium/iron-list/README.md @@ -7,6 +7,9 @@ iron-list.html Edit those files, and our readme bot will duplicate them over here! Edit this file, and the bot will squash your changes :) +The bot does some handling of markdown. Please file a bug if it does the wrong +thing! https://github.com/PolymerLabs/tedium/issues + --> [![Build Status](https://travis-ci.org/PolymerElements/iron-list.svg?branch=master)](https://travis-ci.org/PolymerElements/iron-list) @@ -16,8 +19,6 @@ _[Demo and API Docs](https://elements.polymer-project.org/elements/iron-list)_ ##<iron-list> - - `iron-list` displays a virtual, 'infinite' list. The template inside the iron-list element represents the DOM to create for each list item. The `items` property specifies an array of list item data. @@ -39,12 +40,14 @@ layout means (e.g. the `flex` or `fit` classes). List item templates should bind to template models of the following structure: - { - index: 0, // data index for this item - item: { // user data corresponding to items[index] - /* user item data */ - } - } +```js +{ + index: 0, // index in the item array + selected: false, // true if the current item is selected + tabIndex: -1, // a dynamically generated tabIndex for focus management + item: {} // user data corresponding to items[index] +} +``` Alternatively, you can change the property name used as data index by changing the `indexAs` property. The `as` property defines the name of the variable to add to the binding @@ -71,16 +74,33 @@ bound from the model object provided to the template scope): <iron-list items="[[data]]" as="item"> <template> <div> - Name: <span>[[item.name]]</span> + Name: [[item.name]] </div> </template> </iron-list> </template> ``` +### Accessibility + +`iron-list` automatically manages the focus state for the items. It also provides +a `tabIndex` property within the template scope that can be used for keyboard navigation. +For example, users can press the up and down keys to move to previous and next +items in the list: + +```html +<iron-list items="[[data]]" as="item"> + <template> + <div tabindex$="[[tabIndex]]"> + Name: [[item.name]] + </div> + </template> +</iron-list> +``` + ### Styling -Use the `--iron-list-items-container` mixin to style the container of items, e.g. +You can use the `--iron-list-items-container` mixin to style the container of items: ```css iron-list { @@ -98,7 +118,7 @@ This event is fired by any element that implements `IronResizableBehavior`. By default, elements such as `iron-pages`, `paper-tabs` or `paper-dialog` will trigger this event automatically. If you hide the list manually (e.g. you use `display: none`) you might want to implement `IronResizableBehavior` or fire this event manually right -after the list became visible again. e.g. +after the list became visible again. For example: ```js document.querySelector('iron-list').fire('iron-resize'); @@ -111,7 +131,7 @@ visible on the screen. e.g. the page has 500 nodes, but only 20 are visible at t This is why we refer to it as a `virtual` list. In this case, a `dom-repeat` will still create 500 nodes which could slow down the web app, but `iron-list` will only create 20. -However, having an `iron-list` does not mean that you can load all the data at once. +However, having an `iron-list` does not mean that you can load all the data at once. Say, you have a million records in the database, you want to split the data into pages so you can bring a page at the time. The page could contain 500 items, and iron-list will only render 20. diff --git a/third_party/polymer/v1_0/components-chromium/iron-list/bower.json b/third_party/polymer/v1_0/components-chromium/iron-list/bower.json index 607bd606..50bb130 100644 --- a/third_party/polymer/v1_0/components-chromium/iron-list/bower.json +++ b/third_party/polymer/v1_0/components-chromium/iron-list/bower.json @@ -7,7 +7,7 @@ "list", "virtual-list" ], - "version": "1.1.7", + "version": "1.2.3", "homepage": "https://github.com/PolymerElements/iron-list", "authors": [ "The Polymer Authors" @@ -21,25 +21,27 @@ "ignore": [], "dependencies": { "polymer": "Polymer/polymer#^1.1.0", - "iron-resizable-behavior": "polymerelements/iron-resizable-behavior#^1.0.0" + "iron-resizable-behavior": "polymerelements/iron-resizable-behavior#^1.0.0", + "iron-a11y-keys-behavior": "polymerelements/iron-a11y-keys-behavior#^1.0.0", + "iron-scroll-target-behavior": "PolymerElements/iron-scroll-target-behavior#^1.0.0" }, "devDependencies": { + "app-layout": "PolymerLabs/app-layout#master", "iron-flex-layout": "polymerelements/iron-flex-layout#^1.0.0", "iron-component-page": "polymerelements/iron-component-page#^1.0.0", - "paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0", + "iron-ajax": "polymerelements/iron-ajax#^1.0.0", + "iron-icon": "polymerelements/iron-icon#^1.0.0", + "iron-icons": "polymerelements/iron-icons#^1.0.0", + "iron-scroll-threshold": "polymerelements/iron-scroll-threshold#^1.0.0", "paper-menu": "polymerelements/paper-menu#^1.0.0", "paper-item": "polymerelements/paper-item#^1.0.0", "paper-icon-button": "polymerelements/paper-icon-button#^1.0.0", "paper-button": "polymerelements/paper-button#^1.0.0", - "iron-ajax": "polymerelements/iron-ajax#^1.0.0", - "iron-icon": "polymerelements/iron-icon#^1.0.0", - "iron-icons": "polymerelements/iron-icons#^1.0.0", "paper-badge": "polymerelements/paper-badge#^1.0.0", - "paper-toolbar": "polymerelements/paper-toolbar#^1.0.0", "paper-spinner": "polymerelements/paper-spinner#^1.0.0", "test-fixture": "polymerelements/test-fixture#^1.0.0", "iron-test-helpers": "polymerelements/iron-test-helpers#^1.0.0", "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.2", - "web-component-tester": "polymer/web-component-tester#^3.4.0" + "web-component-tester": "^4.0.0" } } diff --git a/third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js b/third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js index a7c75b8..8a0ee87 100644 --- a/third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js +++ b/third_party/polymer/v1_0/components-chromium/iron-list/iron-list-extracted.js @@ -4,6 +4,7 @@ var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; var DEFAULT_PHYSICAL_COUNT = 3; var MAX_PHYSICAL_COUNT = 500; + var HIDDEN_Y = '-10000px'; Polymer({ @@ -89,18 +90,27 @@ observers: [ '_itemsChanged(items.*)', '_selectionEnabledChanged(selectionEnabled)', - '_multiSelectionChanged(multiSelection)' + '_multiSelectionChanged(multiSelection)', + '_setOverflow(scrollTarget)' ], behaviors: [ Polymer.Templatizer, - Polymer.IronResizableBehavior + Polymer.IronResizableBehavior, + Polymer.IronA11yKeysBehavior, + Polymer.IronScrollTargetBehavior ], listeners: { 'iron-resize': '_resizeHandler' }, + keyBindings: { + 'up': '_didMoveUp', + 'down': '_didMoveDown', + 'enter': '_didEnter' + }, + /** * The ratio of hidden tiles that should remain in the scroll direction. * Recommended value ~0.5, so it will distribute tiles evely in both directions. @@ -108,12 +118,6 @@ _ratio: 0.5, /** - * The element that controls the scroll - * @type {?Element} - */ - _scroller: null, - - /** * The padding-top value of the `scroller` element */ _scrollerPaddingTop: 0, @@ -144,7 +148,7 @@ _physicalSize: 0, /** - * The average `offsetHeight` of the tiles observed till now. + * The average `F` of the tiles observed till now. */ _physicalAverage: 0, @@ -202,13 +206,21 @@ _physicalSizes: null, /** - * A cached value for the visible index. + * A cached value for the first visible index. * See `firstVisibleIndex` * @type {?number} */ _firstVisibleIndexVal: null, /** + * A cached value for the last visible index. + * See `lastVisibleIndex` + * @type {?number} + */ + _lastVisibleIndexVal: null, + + + /** * A Polymer collection for the items. * @type {?Polymer.Collection} */ @@ -231,6 +243,23 @@ _maxPages: 3, /** + * The currently focused item index. + */ + _focusedIndex: 0, + + /** + * The the item that is focused if it is moved offscreen. + * @private {?TemplatizerNode} + */ + _offscreenFocusedItem: null, + + /** + * The item that backfills the `_offscreenFocusedItem` in the physical items + * list when that item is moved offscreen. + */ + _focusBackfillItem: null, + + /** * The bottom of the physical content. */ get _physicalBottom() { @@ -248,7 +277,7 @@ * The n-th item rendered in the last physical item. */ get _virtualEnd() { - return this._virtualStartVal + this._physicalCount - 1; + return this._virtualStart + this._physicalCount - 1; }, /** @@ -283,8 +312,13 @@ set _virtualStart(val) { // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val)); - this._physicalStart = this._virtualStartVal % this._physicalCount; - this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount; + if (this._physicalCount === 0) { + this._physicalStart = 0; + this._physicalEnd = 0; + } else { + this._physicalStart = this._virtualStartVal % this._physicalCount; + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount; + } }, /** @@ -309,7 +343,7 @@ * True if the current list is visible. */ get _isVisible() { - return this._scroller && Boolean(this._scroller.offsetWidth || this._scroller.offsetHeight); + return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight); }, /** @@ -318,10 +352,8 @@ * @type {number} */ get firstVisibleIndex() { - var physicalOffset; - if (this._firstVisibleIndexVal === null) { - physicalOffset = this._physicalTop; + var physicalOffset = this._physicalTop; this._firstVisibleIndexVal = this._iterateItems( function(pidx, vidx) { @@ -332,54 +364,52 @@ } }) || 0; } - return this._firstVisibleIndexVal; }, - ready: function() { - if (IOS_TOUCH_SCROLLING) { - this._scrollListener = function() { - requestAnimationFrame(this._scrollHandler.bind(this)); - }.bind(this); - } else { - this._scrollListener = this._scrollHandler.bind(this); - } - }, - /** - * When the element has been attached to the DOM tree. + * Gets the index of the last visible item in the viewport. + * + * @type {number} */ - attached: function() { - // delegate to the parent's scroller - // e.g. paper-scroll-header-panel - var el = Polymer.dom(this); + get lastVisibleIndex() { + if (this._lastVisibleIndexVal === null) { + var physicalOffset = this._physicalTop; - var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode); - if (parentNode && parentNode.scroller) { - this._scroller = parentNode.scroller; - } else { - this._scroller = this; - this.classList.add('has-scroller'); - } + this._iterateItems(function(pidx, vidx) { + physicalOffset += this._physicalSizes[pidx]; - if (IOS_TOUCH_SCROLLING) { - this._scroller.style.webkitOverflowScrolling = 'touch'; + if(physicalOffset <= this._scrollBottom) { + this._lastVisibleIndexVal = vidx; + } + }); } + return this._lastVisibleIndexVal; + }, - this._scroller.addEventListener('scroll', this._scrollListener); + ready: function() { + this.addEventListener('focus', this._didFocus.bind(this), true); + }, + attached: function() { this.updateViewportBoundaries(); this._render(); }, - /** - * When the element has been removed from the DOM tree. - */ detached: function() { this._itemsRendered = false; - if (this._scroller) { - this._scroller.removeEventListener('scroll', this._scrollListener); - } + }, + + get _defaultScrollTarget() { + return this; + }, + + /** + * Set the overflow property if this element has its own scrolling region + */ + _setOverflow: function(scrollTarget) { + this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : ''; + this.style.overflow = scrollTarget === this ? 'auto' : ''; }, /** @@ -389,20 +419,20 @@ * @method updateViewportBoundaries */ updateViewportBoundaries: function() { - var scrollerStyle = window.getComputedStyle(this._scroller); + var scrollerStyle = window.getComputedStyle(this.scrollTarget); this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10); - this._viewportSize = this._scroller.offsetHeight; + this._viewportSize = this._scrollTargetHeight; }, /** * Update the models, the position of the * items in the viewport and recycle tiles as needed. */ - _refresh: function() { + _scrollHandler: function() { // clamp the `scrollTop` value // IE 10|11 scrollTop may go above `_maxScrollTop` // iOS `scrollTop` may go below 0 and above `_maxScrollTop` - var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.scrollTop)); + var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop)); var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom; var ratio = this._ratio; var delta = scrollTop - this._scrollPosition; @@ -416,6 +446,7 @@ // clear cached visible index this._firstVisibleIndexVal = null; + this._lastVisibleIndexVal = null; scrollBottom = this._scrollBottom; physicalBottom = this._physicalBottom; @@ -505,17 +536,21 @@ }, /** - * Update the list of items, starting from the `_virtualStartVal` item. + * Update the list of items, starting from the `_virtualStart` item. * @param {!Array<number>=} itemSet * @param {!Array<number>=} movingUp */ _update: function(itemSet, movingUp) { + // manage focus + if (this._isIndexRendered(this._focusedIndex)) { + this._restoreFocusedItem(); + } else { + this._createFocusBackfillItem(); + } // update models this._assignModels(itemSet); - // measure heights this._updateMetrics(itemSet); - // adjust offset after measuring if (movingUp) { while (movingUp.length) { @@ -524,10 +559,8 @@ } // update the position of the items this._positionItems(); - // set the scroller size this._updateScrollerSize(); - // increase the pool of physical items this._increasePoolIfNeeded(); }, @@ -547,7 +580,6 @@ physicalItems[i] = inst.root.querySelector('*'); Polymer.dom(this).appendChild(inst.root); } - return physicalItems; }, @@ -557,24 +589,24 @@ * if the physical size is shorter than `_optPhysicalSize` */ _increasePoolIfNeeded: function() { - if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize) { - // 0 <= `currentPage` <= `_maxPages` - var currentPage = Math.floor(this._physicalSize / this._viewportSize); - - if (currentPage === 0) { - // fill the first page - this.async(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5))); - } else if (this._lastPage !== currentPage) { - // once a page is filled up, paint it and defer the next increase - requestAnimationFrame(this._increasePool.bind(this, 1)); - } else { - // fill the rest of the pages - this.async(this._increasePool.bind(this, 1)); - } - this._lastPage = currentPage; - return true; + if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) { + return false; + } + // 0 <= `currentPage` <= `_maxPages` + var currentPage = Math.floor(this._physicalSize / this._viewportSize); + if (currentPage === 0) { + // fill the first page + this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5))); + } else if (this._lastPage !== currentPage) { + // paint the page and defer the next increase + // wait 16ms which is rough enough to get paint cycle. + Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16)); + } else { + // fill the rest of the pages + this._debounceTemplate(this._increasePool.bind(this, 1)); } - return false; + this._lastPage = currentPage; + return true; }, /** @@ -584,7 +616,7 @@ // limit the size var nextPhysicalCount = Math.min( this._physicalCount + missingItems, - this._virtualCount, + this._virtualCount - this._virtualStart, MAX_PHYSICAL_COUNT ); var prevPhysicalCount = this._physicalCount; @@ -610,6 +642,7 @@ if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) { this._lastPage = 0; this._update(); + this._scrollHandler(); this._itemsRendered = true; } }, @@ -621,11 +654,11 @@ if (!this.ctor) { // Template instance props that should be excluded from forwarding var props = {}; - props.__key__ = true; props[this.as] = true; props[this.indexAs] = true; props[this.selectedAs] = true; + props.tabIndex = true; this._instanceProps = props; this._userTemplate = Polymer.dom(this).querySelector('template'); @@ -693,6 +726,10 @@ var key = path.substring(0, dot < 0 ? path.length : dot); var idx = this._physicalIndexForKey[key]; var row = this._physicalItems[idx]; + + if (idx === this._focusedIndex && this._offscreenFocusedItem) { + row = this._offscreenFocusedItem; + } if (row) { var inst = row._templateInstance; if (dot >= 0) { @@ -711,17 +748,18 @@ */ _itemsChanged: function(change) { if (change.path === 'items') { + + this._restoreFocusedItem(); // render the new set this._itemsRendered = false; - // update the whole set - this._virtualStartVal = 0; + this._virtualStart = 0; this._physicalTop = 0; this._virtualCount = this.items ? this.items.length : 0; + this._focusedIndex = 0; this._collection = this.items ? Polymer.Collection.get(this.items) : null; this._physicalIndexForKey = {}; - // scroll to the top this._resetScrollPosition(0); // create the initial physical items @@ -730,17 +768,20 @@ this._physicalItems = this._createPool(this._physicalCount); this._physicalSizes = new Array(this._physicalCount); } - - this.debounce('refresh', this._render); + this._debounceTemplate(this._render); } else if (change.path === 'items.splices') { // render the new set this._itemsRendered = false; - this._adjustVirtualIndex(change.value.indexSplices); this._virtualCount = this.items ? this.items.length : 0; - this.debounce('refresh', this._render); + this._debounceTemplate(this._render); + + if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) { + this._focusedIndex = 0; + } + this._debounceTemplate(this._render); } else { // update a single item @@ -762,19 +803,15 @@ idx = splice.index; // We only need to care about changes happening above the current position - if (idx >= this._virtualStartVal) { + if (idx >= this._virtualStart) { break; } this._virtualStart = this._virtualStart + - Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStartVal); + Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart); } }, - _scrollHandler: function() { - this._refresh(); - }, - /** * Executes a provided function per every physical index in `itemSet` * `itemSet` default value is equivalent to the entire set of physical indexes. @@ -789,9 +826,9 @@ for (i = 0; i < itemSet.length; i++) { pidx = itemSet[i]; if (pidx >= this._physicalStart) { - vidx = this._virtualStartVal + (pidx - this._physicalStart); + vidx = this._virtualStart + (pidx - this._physicalStart); } else { - vidx = this._virtualStartVal + (this._physicalCount - this._physicalStart) + pidx; + vidx = this._virtualStart + (this._physicalCount - this._physicalStart) + pidx; } if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; @@ -799,17 +836,14 @@ } } else { pidx = this._physicalStart; - vidx = this._virtualStartVal; + vidx = this._virtualStart; for (; pidx < this._physicalCount; pidx++, vidx++) { if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; } } - - pidx = 0; - - for (; pidx < this._physicalStart; pidx++, vidx++) { + for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) { if ((rtn = fn.call(this, pidx, vidx)) != null) { return rtn; } @@ -827,12 +861,12 @@ var inst = el._templateInstance; var item = this.items && this.items[vidx]; - if (item) { + if (item !== undefined && item !== null) { inst[this.as] = item; inst.__key__ = this._collection.getKey(item); - inst[this.selectedAs] = - /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item); + inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item); inst[this.indexAs] = vidx; + inst.tabIndex = vidx === this._focusedIndex ? 0 : -1; el.removeAttribute('hidden'); this._physicalIndexForKey[inst.__key__] = pidx; } else { @@ -849,23 +883,26 @@ * @param {!Array<number>=} itemSet */ _updateMetrics: function(itemSet) { + // Make sure we distributed all the physical items + // so we can measure them + Polymer.dom.flush(); + var newPhysicalSize = 0; var oldPhysicalSize = 0; var prevAvgCount = this._physicalAverageCount; var prevPhysicalAvg = this._physicalAverage; - // Make sure we distributed all the physical items - // so we can measure them - Polymer.dom.flush(); this._iterateItems(function(pidx, vidx) { + oldPhysicalSize += this._physicalSizes[pidx] || 0; this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight; newPhysicalSize += this._physicalSizes[pidx]; this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0; + }, itemSet); this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize; - this._viewportSize = this._scroller.offsetHeight; + this._viewportSize = this._scrollTargetHeight; // update the average if we measured something if (this._physicalAverageCount !== prevAvgCount) { @@ -885,7 +922,7 @@ this._iterateItems(function(pidx) { - this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pidx]); + this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]); y += this._physicalSizes[pidx]; }); @@ -895,15 +932,14 @@ * Adjusts the scroll position when it was overestimated. */ _adjustScrollPosition: function() { - var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop : + var deltaHeight = this._virtualStart === 0 ? this._physicalTop : Math.min(this._scrollPosition + this._physicalTop, 0); if (deltaHeight) { this._physicalTop = this._physicalTop - deltaHeight; - // juking scroll position during interial scrolling on iOS is no bueno if (!IOS_TOUCH_SCROLLING) { - this._resetScrollPosition(this._scroller.scrollTop - deltaHeight); + this._resetScrollPosition(this._scrollTop - deltaHeight); } } }, @@ -912,9 +948,9 @@ * Sets the position of the scroll. */ _resetScrollPosition: function(pos) { - if (this._scroller) { - this._scroller.scrollTop = pos; - this._scrollPosition = this._scroller.scrollTop; + if (this.scrollTarget) { + this._scrollTop = pos; + this._scrollPosition = this._scrollTop; } }, @@ -925,7 +961,7 @@ */ _updateScrollerSize: function(forceUpdate) { this._estScrollHeight = (this._physicalBottom + - Math.max(this._virtualCount - this._physicalCount - this._virtualStartVal, 0) * this._physicalAverage); + Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage); forceUpdate = forceUpdate || this._scrollHeight === 0; forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize; @@ -949,20 +985,18 @@ return; } - var firstVisible = this.firstVisibleIndex; + Polymer.dom.flush(); + var firstVisible = this.firstVisibleIndex; idx = Math.min(Math.max(idx, 0), this._virtualCount-1); // start at the previous virtual item // so we have a item above the first visible item this._virtualStart = idx - 1; - // assign new models this._assignModels(); - // measure the new sizes this._updateMetrics(); - // estimate new physical offset this._physicalTop = this._virtualStart * this._physicalAverage; @@ -977,21 +1011,17 @@ currentTopItem = (currentTopItem + 1) % this._physicalCount; currentVirtualItem++; } - // update the scroller size this._updateScrollerSize(true); - // update the position of the items this._positionItems(); - // set the new scroll position - this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1); - + this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1); // increase the pool of physical items if needed this._increasePoolIfNeeded(); - // clear cached visible index this._firstVisibleIndexVal = null; + this._lastVisibleIndexVal = null; }, /** @@ -1007,7 +1037,11 @@ * when the element is resized. */ _resizeHandler: function() { - this.debounce('resize', function() { + // iOS fires the resize event when the address bar slides up + if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) { + return; + } + this._debounceTemplate(function() { this._render(); if (this._itemsRendered && this._physicalItems && this._isVisible) { this._resetAverage(); @@ -1033,12 +1067,14 @@ * @param {(Object|number)} item The item object or its index */ _getNormalizedItem: function(item) { - if (typeof item === 'number') { - item = this.items[item]; - if (!item) { - throw new RangeError('<item> not found'); + if (this._collection.getKey(item) === undefined) { + if (typeof item === 'number') { + item = this.items[item]; + if (!item) { + throw new RangeError('<item> not found'); + } + return item; } - } else if (this._collection.getKey(item) === undefined) { throw new TypeError('<item> should be a valid item'); } return item; @@ -1061,6 +1097,7 @@ model[this.selectedAs] = true; } this.$.selector.select(item); + this.updateSizeForItem(item); }, /** @@ -1078,6 +1115,7 @@ model[this.selectedAs] = false; } this.$.selector.deselect(item); + this.updateSizeForItem(item); }, /** @@ -1123,20 +1161,15 @@ * it will remove the listener otherwise. */ _selectionEnabledChanged: function(selectionEnabled) { - if (selectionEnabled) { - this.listen(this, 'tap', '_selectionHandler'); - this.listen(this, 'keypress', '_selectionHandler'); - } else { - this.unlisten(this, 'tap', '_selectionHandler'); - this.unlisten(this, 'keypress', '_selectionHandler'); - } + var handler = selectionEnabled ? this.listen : this.unlisten; + handler.call(this, this, 'tap', '_selectionHandler'); }, /** * Select an item from an event object. */ _selectionHandler: function(e) { - if (e.type !== 'keypress' || e.keyCode === 13) { + if (this.selectionEnabled) { var model = this.modelForElement(e.target); if (model) { this.toggleSelectionForItem(model[this.as]); @@ -1164,7 +1197,136 @@ this._updateMetrics([pidx]); this._positionItems(); } + }, + + _isIndexRendered: function(idx) { + return idx >= this._virtualStart && idx <= this._virtualEnd; + }, + + _getPhysicalItemForIndex: function(idx, force) { + if (!this._collection) { + return null; + } + if (!this._isIndexRendered(idx)) { + if (force) { + this.scrollToIndex(idx); + return this._getPhysicalItemForIndex(idx, false); + } + return null; + } + var item = this._getNormalizedItem(idx); + var physicalItem = this._physicalItems[this._physicalIndexForKey[this._collection.getKey(item)]]; + + return physicalItem || null; + }, + + _focusPhysicalItem: function(idx) { + this._restoreFocusedItem(); + + var physicalItem = this._getPhysicalItemForIndex(idx, true); + if (!physicalItem) { + return; + } + var SECRET = ~(Math.random() * 100); + var model = physicalItem._templateInstance; + var focusable; + + model.tabIndex = SECRET; + // the focusable element could be the entire physical item + if (physicalItem.tabIndex === SECRET) { + focusable = physicalItem; + } + // the focusable element could be somewhere within the physical item + if (!focusable) { + focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]'); + } + // restore the tab index + model.tabIndex = 0; + focusable && focusable.focus(); + }, + + _restoreFocusedItem: function() { + if (!this._offscreenFocusedItem) { + return; + } + var item = this._getNormalizedItem(this._focusedIndex); + var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; + + if (pidx !== undefined) { + this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]); + this._physicalItems[pidx] = this._offscreenFocusedItem; + } + this._offscreenFocusedItem = null; + }, + + _removeFocusedItem: function() { + if (!this._offscreenFocusedItem) { + return; + } + Polymer.dom(this).removeChild(this._offscreenFocusedItem); + this._offscreenFocusedItem = null; + this._focusBackfillItem = null; + }, + + _createFocusBackfillItem: function() { + if (this._offscreenFocusedItem) { + return; + } + var item = this._getNormalizedItem(this._focusedIndex); + var pidx = this._physicalIndexForKey[this._collection.getKey(item)]; + + this._offscreenFocusedItem = this._physicalItems[pidx]; + this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem); + + if (!this._focusBackfillItem) { + var stampedTemplate = this.stamp(null); + this._focusBackfillItem = stampedTemplate.root.querySelector('*'); + Polymer.dom(this).appendChild(stampedTemplate.root); + } + this._physicalItems[pidx] = this._focusBackfillItem; + }, + + _didFocus: function(e) { + var targetModel = this.modelForElement(e.target); + var fidx = this._focusedIndex; + + if (!targetModel) { + return; + } + this._restoreFocusedItem(); + + if (this.modelForElement(this._offscreenFocusedItem) === targetModel) { + this.scrollToIndex(fidx); + } else { + // restore tabIndex for the currently focused item + this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1; + // set the tabIndex for the next focused item + targetModel.tabIndex = 0; + fidx = /** @type {{index: number}} */(targetModel).index; + this._focusedIndex = fidx; + // bring the item into view + if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) { + this.scrollToIndex(fidx); + } else { + this._update(); + } + } + }, + + _didMoveUp: function() { + this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1)); + }, + + _didMoveDown: function() { + this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1)); + }, + + _didEnter: function(e) { + // focus the currently focused physical item + this._focusPhysicalItem(this._focusedIndex); + // toggle selection + this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent); } }); -})();
\ No newline at end of file +})(); diff --git a/third_party/polymer/v1_0/components-chromium/iron-list/iron-list.html b/third_party/polymer/v1_0/components-chromium/iron-list/iron-list.html index 9b3148a..6737eea 100644 --- a/third_party/polymer/v1_0/components-chromium/iron-list/iron-list.html +++ b/third_party/polymer/v1_0/components-chromium/iron-list/iron-list.html @@ -8,6 +8,8 @@ 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 --><html><head><link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html"> +<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> +<link rel="import" href="../iron-scroll-target-behavior/iron-scroll-target-behavior.html"> <!-- @@ -32,12 +34,14 @@ layout means (e.g. the `flex` or `fit` classes). List item templates should bind to template models of the following structure: - { - index: 0, // data index for this item - item: { // user data corresponding to items[index] - /* user item data */ - } - } +```js +{ + index: 0, // index in the item array + selected: false, // true if the current item is selected + tabIndex: -1, // a dynamically generated tabIndex for focus management + item: {} // user data corresponding to items[index] +} +``` Alternatively, you can change the property name used as data index by changing the `indexAs` property. The `as` property defines the name of the variable to add to the binding @@ -64,16 +68,33 @@ bound from the model object provided to the template scope): <iron-list items="[[data]]" as="item"> <template> <div> - Name: <span>[[item.name]]</span> + Name: [[item.name]] </div> </template> </iron-list> </template> ``` +### Accessibility + +`iron-list` automatically manages the focus state for the items. It also provides +a `tabIndex` property within the template scope that can be used for keyboard navigation. +For example, users can press the up and down keys to move to previous and next +items in the list: + +```html +<iron-list items="[[data]]" as="item"> + <template> + <div tabindex$="[[tabIndex]]"> + Name: [[item.name]] + </div> + </template> +</iron-list> +``` + ### Styling -Use the `--iron-list-items-container` mixin to style the container of items, e.g. +You can use the `--iron-list-items-container` mixin to style the container of items: ```css iron-list { @@ -91,7 +112,7 @@ This event is fired by any element that implements `IronResizableBehavior`. By default, elements such as `iron-pages`, `paper-tabs` or `paper-dialog` will trigger this event automatically. If you hide the list manually (e.g. you use `display: none`) you might want to implement `IronResizableBehavior` or fire this event manually right -after the list became visible again. e.g. +after the list became visible again. For example: ```js document.querySelector('iron-list').fire('iron-resize'); @@ -104,7 +125,7 @@ visible on the screen. e.g. the page has 500 nodes, but only 20 are visible at t This is why we refer to it as a `virtual` list. In this case, a `dom-repeat` will still create 500 nodes which could slow down the web app, but `iron-list` will only create 20. -However, having an `iron-list` does not mean that you can load all the data at once. +However, having an `iron-list` does not mean that you can load all the data at once. Say, you have a million records in the database, you want to split the data into pages so you can bring a page at the time. The page could contain 500 items, and iron-list will only render 20. @@ -114,6 +135,8 @@ will only render 20. @demo demo/index.html Simple list @demo demo/selection.html Selection of items @demo demo/collapse.html Collapsable items +@demo demo/scroll-threshold.html Scroll thesholds + --> </head><body><dom-module id="iron-list"> @@ -121,14 +144,13 @@ will only render 20. <style> :host { display: block; + position: relative; } - :host(.has-scroller) { - overflow: auto; - } - - :host(:not(.has-scroller)) { - position: relative; + @media only screen and (-webkit-max-device-pixel-ratio: 1) { + :host { + will-change: transform; + } } #items { diff --git a/third_party/polymer/v1_0/components-chromium/paper-dialog/.bower.json b/third_party/polymer/v1_0/components-chromium/paper-dialog/.bower.json index 90f438f..2882e47 100644 --- a/third_party/polymer/v1_0/components-chromium/paper-dialog/.bower.json +++ b/third_party/polymer/v1_0/components-chromium/paper-dialog/.bower.json @@ -1,7 +1,7 @@ { "name": "paper-dialog", "description": "A Material Design dialog", - "version": "1.0.3", + "version": "1.0.4", "authors": "The Polymer Authors", "keywords": [ "web-components", @@ -19,27 +19,27 @@ "homepage": "https://github.com/PolymerElements/paper-dialog", "ignore": [], "dependencies": { - "polymer": "Polymer/polymer#^1.1.0", + "neon-animation": "PolymerElements/neon-animation#^1.0.0", "paper-dialog-behavior": "PolymerElements/paper-dialog-behavior#^1.0.0", "paper-styles": "PolymerElements/paper-styles#^1.0.0", - "neon-animation": "PolymerElements/neon-animation#^1.0.0" + "polymer": "Polymer/polymer#^1.1.0" }, "devDependencies": { "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", "paper-button": "PolymerElements/paper-button#^1.0.0", + "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.0.0", "paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^1.0.0", - "paper-menu": "PolymerElements/paper-menu#^1.0.0", "paper-item": "PolymerElements/paper-item#^1.0.0", - "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.0.0", + "paper-menu": "PolymerElements/paper-menu#^1.0.0", "test-fixture": "PolymerElements/test-fixture#^1.0.0", - "web-component-tester": "polymer/web-component-tester#^3.4.0", + "web-component-tester": "^4.0.0", "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" }, - "_release": "1.0.3", + "_release": "1.0.4", "_resolution": { "type": "version", - "tag": "v1.0.3", - "commit": "ec85b110e11e9d390f601e1c88ebf97984b53d82" + "tag": "v1.0.4", + "commit": "53b099bed06bbbab7cb0f82c8209328c1a82aee6" }, "_source": "git://github.com/PolymerElements/paper-dialog.git", "_target": "^1.0.0", diff --git a/third_party/polymer/v1_0/components-chromium/paper-dialog/CONTRIBUTING.md b/third_party/polymer/v1_0/components-chromium/paper-dialog/CONTRIBUTING.md index 7b10141..f147978a 100644 --- a/third_party/polymer/v1_0/components-chromium/paper-dialog/CONTRIBUTING.md +++ b/third_party/polymer/v1_0/components-chromium/paper-dialog/CONTRIBUTING.md @@ -5,6 +5,11 @@ https://github.com/PolymerElements/ContributionGuide/blob/master/CONTRIBUTING.md If you edit that file, it will get updated everywhere else. If you edit this file, your changes will get overridden :) + +You can however override the jsbin link with one that's customized to this +specific element: + +jsbin=https://jsbin.com/cagaye/edit?html,output --> # Polymer Elements ## Guide for Contributors @@ -41,7 +46,7 @@ Polymer Elements are built in the open, and the Polymer authors eagerly encourag 3. Click the `paper-foo` element. ``` - 2. **A reduced test case that demonstrates the problem.** If possible, please include the test case as a JSBin. Start with this template to easily import and use relevant Polymer Elements: [http://jsbin.com/cagaye](http://jsbin.com/cagaye/edit?html,output). + 2. **A reduced test case that demonstrates the problem.** If possible, please include the test case as a JSBin. Start with this template to easily import and use relevant Polymer Elements: [https://jsbin.com/cagaye/edit?html,output](https://jsbin.com/cagaye/edit?html,output). 3. **A list of browsers where the problem occurs.** This can be skipped if the problem is the same across all browsers. @@ -51,14 +56,14 @@ Polymer Elements are built in the open, and the Polymer authors eagerly encourag When submitting pull requests, please provide: - 1. **A reference to the corresponding issue** or issues that will be closed by the pull request. Please refer to these issues using the following syntax: + 1. **A reference to the corresponding issue** or issues that will be closed by the pull request. Please refer to these issues in the pull request description using the following syntax: ```markdown (For a single issue) Fixes #20 (For multiple issues) - Fixes #32, #40 + Fixes #32, fixes #40 ``` 2. **A succinct description of the design** used to fix any related issues. For example: diff --git a/third_party/polymer/v1_0/components-chromium/paper-dialog/README.md b/third_party/polymer/v1_0/components-chromium/paper-dialog/README.md index 4bcd5d4..787cb7e 100644 --- a/third_party/polymer/v1_0/components-chromium/paper-dialog/README.md +++ b/third_party/polymer/v1_0/components-chromium/paper-dialog/README.md @@ -7,16 +7,18 @@ paper-dialog.html Edit those files, and our readme bot will duplicate them over here! Edit this file, and the bot will squash your changes :) +The bot does some handling of markdown. Please file a bug if it does the wrong +thing! https://github.com/PolymerLabs/tedium/issues + --> -[![Build Status](https://travis-ci.org/PolymerElements/paper-dialog.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dialog) +[![Build status](https://travis-ci.org/PolymerElements/paper-dialog.svg?branch=master)](https://travis-ci.org/PolymerElements/paper-dialog) -_[Demo and API Docs](https://elements.polymer-project.org/elements/paper-dialog)_ +_[Demo and API docs](https://elements.polymer-project.org/elements/paper-dialog)_ ##<paper-dialog> - Material design: [Dialogs](https://www.google.com/design/spec/components/dialogs.html) `<paper-dialog>` is a dialog with Material Design styling and optional animations when it is @@ -27,16 +29,18 @@ content area. See `Polymer.PaperDialogBehavior` for specifics. For example, the following code implements a dialog with a header, scrolling content area and buttons. - <paper-dialog> - <h2>Header</h2> - <paper-dialog-scrollable> - Lorem ipsum... - </paper-dialog-scrollable> - <div class="buttons"> - <paper-button dialog-dismiss>Cancel</paper-button> - <paper-button dialog-confirm>Accept</paper-button> - </div> - </paper-dialog> +```html +<paper-dialog> + <h2>Header</h2> + <paper-dialog-scrollable> + Lorem ipsum... + </paper-dialog-scrollable> + <div class="buttons"> + <paper-button dialog-dismiss>Cancel</paper-button> + <paper-button dialog-confirm>Accept</paper-button> + </div> +</paper-dialog> +``` ### Styling @@ -51,14 +55,16 @@ is opened or closed. See the documentation in For example: - <link rel="import" href="components/neon-animation/animations/scale-up-animation.html"> - <link rel="import" href="components/neon-animation/animations/fade-out-animation.html"> +```html +<link rel="import" href="components/neon-animation/animations/scale-up-animation.html"> +<link rel="import" href="components/neon-animation/animations/fade-out-animation.html"> - <paper-dialog entry-animation="scale-up-animation" - exit-animation="fade-out-animation"> - <h2>Header</h2> - <div>Dialog body</div> - </paper-dialog> +<paper-dialog entry-animation="scale-up-animation" + exit-animation="fade-out-animation"> + <h2>Header</h2> + <div>Dialog body</div> +</paper-dialog> +``` ### Accessibility diff --git a/third_party/polymer/v1_0/components-chromium/paper-dialog/bower.json b/third_party/polymer/v1_0/components-chromium/paper-dialog/bower.json index ae1aa53..ab3be84 100644 --- a/third_party/polymer/v1_0/components-chromium/paper-dialog/bower.json +++ b/third_party/polymer/v1_0/components-chromium/paper-dialog/bower.json @@ -1,7 +1,7 @@ { "name": "paper-dialog", "description": "A Material Design dialog", - "version": "1.0.3", + "version": "1.0.4", "authors": "The Polymer Authors", "keywords": [ "web-components", @@ -19,20 +19,20 @@ "homepage": "https://github.com/PolymerElements/paper-dialog", "ignore": [], "dependencies": { - "polymer": "Polymer/polymer#^1.1.0", + "neon-animation": "PolymerElements/neon-animation#^1.0.0", "paper-dialog-behavior": "PolymerElements/paper-dialog-behavior#^1.0.0", "paper-styles": "PolymerElements/paper-styles#^1.0.0", - "neon-animation": "PolymerElements/neon-animation#^1.0.0" + "polymer": "Polymer/polymer#^1.1.0" }, "devDependencies": { "iron-component-page": "PolymerElements/iron-component-page#^1.0.0", "paper-button": "PolymerElements/paper-button#^1.0.0", + "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.0.0", "paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^1.0.0", - "paper-menu": "PolymerElements/paper-menu#^1.0.0", "paper-item": "PolymerElements/paper-item#^1.0.0", - "paper-dialog-scrollable": "PolymerElements/paper-dialog-scrollable#^1.0.0", + "paper-menu": "PolymerElements/paper-menu#^1.0.0", "test-fixture": "PolymerElements/test-fixture#^1.0.0", - "web-component-tester": "polymer/web-component-tester#^3.4.0", + "web-component-tester": "^4.0.0", "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" } } diff --git a/third_party/polymer/v1_0/components-chromium/paper-dialog/paper-dialog-extracted.js b/third_party/polymer/v1_0/components-chromium/paper-dialog/paper-dialog-extracted.js index a91d6ce..1b3570a 100644 --- a/third_party/polymer/v1_0/components-chromium/paper-dialog/paper-dialog-extracted.js +++ b/third_party/polymer/v1_0/components-chromium/paper-dialog/paper-dialog-extracted.js @@ -14,6 +14,7 @@ }, _renderOpened: function() { + this.cancelAnimation(); if (this.withBackdrop) { this.backdropElement.open(); } @@ -21,6 +22,7 @@ }, _renderClosed: function() { + this.cancelAnimation(); if (this.withBackdrop) { this.backdropElement.close(); } diff --git a/third_party/polymer/v1_0/components_summary.txt b/third_party/polymer/v1_0/components_summary.txt index 0bf0492..3560018 100644 --- a/third_party/polymer/v1_0/components_summary.txt +++ b/third_party/polymer/v1_0/components_summary.txt @@ -111,11 +111,11 @@ Revision: 55d2b39ead32b8d90da538daa1a6681fd9ae89d9 Tree link: https://github.com/PolymerElements/iron-input/tree/1.0.8 Name: iron-list -Version: 1.1.7 +Version: 1.2.3 Repository: git://github.com/PolymerElements/iron-list.git -Tag: v1.1.7 -Revision: a00edace6cac13c16ba6e7a930d29a14b98e44c7 -Tree link: https://github.com/PolymerElements/iron-list/tree/v1.1.7 +Tag: v1.2.3 +Revision: 07957dc10616341606ae030d78bb41492fb7c0cc +Tree link: https://github.com/PolymerElements/iron-list/tree/v1.2.3 Name: iron-media-query Version: 1.0.8 @@ -230,11 +230,11 @@ Revision: ce52f51c537d27f414fec7298b9ebff4248044eb Tree link: https://github.com/PolymerElements/paper-checkbox/tree/v1.1.1 Name: paper-dialog -Version: 1.0.3 +Version: 1.0.4 Repository: git://github.com/PolymerElements/paper-dialog.git -Tag: v1.0.3 -Revision: ec85b110e11e9d390f601e1c88ebf97984b53d82 -Tree link: https://github.com/PolymerElements/paper-dialog/tree/v1.0.3 +Tag: v1.0.4 +Revision: 53b099bed06bbbab7cb0f82c8209328c1a82aee6 +Tree link: https://github.com/PolymerElements/paper-dialog/tree/v1.0.4 Name: paper-dialog-behavior Version: 1.1.1 |