diff --git a/demos/common.js b/demos/common.js new file mode 100644 index 00000000000..d29f76df55c --- /dev/null +++ b/demos/common.js @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as interactivity from './interactivity.js'; + +demoReady((root) => { + interactivity.init(root); +}); + +// Export useful libs to aid debugging/experimentation in the browser's dev tools console. +import * as dom from './dom.js'; +import * as pony from './ponyfill.js'; +import * as util from './util.js'; +export {dom, pony, util}; diff --git a/demos/dom.js b/demos/dom.js new file mode 100644 index 00000000000..d1e7b0b55d2 --- /dev/null +++ b/demos/dom.js @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @param {string} query + * @param {!Document|!Element=} root + * @return {!Array} + */ +export function getAll(query, root = document) { + return [].slice.call(root.querySelectorAll(query)); +} + +/** + * @param {!Window|!Document|!Element} root + * @return {!Document|undefined} + */ +export function getDocument(root) { + return root.ownerDocument || root.document || (root.documentElement ? root : undefined); +} + +/** + * @param {!Window|!Document|!Element} root + * @return {!Window|undefined} + */ +export function getWindow(root) { + const doc = getDocument(root); + return doc.defaultView || doc.parentWindow || (root.document ? root : undefined); +} diff --git a/demos/interactivity.js b/demos/interactivity.js new file mode 100644 index 00000000000..f62ba48f054 --- /dev/null +++ b/demos/interactivity.js @@ -0,0 +1,301 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as dom from './dom.js'; +import * as util from './util.js'; + +const classes = { + TOOLBAR_PROGRESS_BAR_ACTIVE: 'demo-toolbar-progress-bar--active', +}; + +const attrs = { + HOT_SWAP: 'data-hot', + IS_LOADING: 'data-is-loading', +}; + +const ids = { + TOOLBAR_PROGRESS_BAR: 'demo-toolbar-progress-bar', +}; + +/** @abstract */ +export class InteractivityProvider { + constructor(root) { + /** @protected {!Document|!Element} */ + this.root_ = root; + + /** @protected {!Document} */ + this.document_ = dom.getDocument(this.root_); + + /** @protected {!Window} */ + this.window_ = dom.getWindow(this.root_); + } + + lazyInit() {} + + /** + * @param {string} selector + * @param {!Document|!Element=} root + * @return {!Array} + * @protected + */ + querySelectorAll_(selector, root = this.root_) { + return dom.getAll(selector, root); + } +} + +export class ToolbarProvider extends InteractivityProvider { + /** @param {!Document|!Element} root */ + static attachTo(root) { + const instance = new ToolbarProvider(root); + instance.lazyInit(); + return instance; + } + + /** @override */ + lazyInit() { + /** @type {?Element} */ + this.progressBarEl_ = this.root_.getElementById(ids.TOOLBAR_PROGRESS_BAR); + } + + /** @param {boolean} isLoading */ + setIsLoading(isLoading) { + if (!this.progressBarEl_) { + return; + } + + if (isLoading) { + this.progressBarEl_.classList.add(classes.TOOLBAR_PROGRESS_BAR_ACTIVE); + } else { + this.progressBarEl_.classList.remove(classes.TOOLBAR_PROGRESS_BAR_ACTIVE); + } + } +} + +export class HotSwapper extends InteractivityProvider { + /** + * @param {!Document|!Element} root + * @param {!ToolbarProvider} toolbarProvider + */ + static attachTo(root, toolbarProvider) { + const instance = new HotSwapper(root); + instance.lazyInit(toolbarProvider); + return instance; + } + + /** @private {number} */ + static get hotUpdateWaitPeriodMs_() { + return 250; + } + + /** + * @param {!ToolbarProvider} toolbarProvider + * @override + */ + lazyInit(toolbarProvider) { + /** @type {!ToolbarProvider} */ + this.toolbarProvider_ = toolbarProvider; + + /** @type {!Array} */ + this.pendingRequests_ = []; + + this.registerHotUpdateHandler_(); + } + + /** @private */ + registerHotUpdateHandler_() { + const hotSwapAllStylesheets = util.debounce(() => { + this.hotSwapAllStylesheets_(); + }, HotSwapper.hotUpdateWaitPeriodMs_); + + this.window_.addEventListener('message', (evt) => { + if (this.isWebpackRecompileStart_(evt)) { + this.toolbarProvider_.setIsLoading(true); + } else if (this.isWebpackRecompileEnd_(evt)) { + hotSwapAllStylesheets(); + } + }); + } + + /** + * @param {!Event} evt + * @return {boolean} + * @private + */ + isWebpackRecompileStart_(evt) { + return Boolean(evt.data) && evt.data.type === 'webpackInvalid'; + } + + /** + * @param {!Event} evt + * @return {boolean} + * @private + */ + isWebpackRecompileEnd_(evt) { + return typeof evt.data === 'string' && evt.data.indexOf('webpackHotUpdate') === 0; + } + + /** @private */ + hotSwapAllStylesheets_() { + this.querySelectorAll_(`link[${attrs.HOT_SWAP}]:not([${attrs.IS_LOADING}])`).forEach((link) => { + this.hotSwapStylesheet(link); + }); + } + + /** + * @param {!Element} oldLink + * @param {string=} newUri + */ + hotSwapStylesheet(oldLink, newUri) { + const oldUri = oldLink.getAttribute('href'); + + // Reload existing stylesheet + if (!newUri) { + newUri = oldUri; + } + + // Force IE 11 and Edge to bypass the cache and request a fresh copy of the CSS. + newUri = this.bustCache_(newUri); + + this.swapItLikeItsHot_(oldLink, oldUri, newUri); + } + + /** + * @param {!Element} oldLink + * @param {string} oldUri + * @param {string} newUri + * @private + */ + swapItLikeItsHot_(oldLink, oldUri, newUri) { + this.logHotSwap_('swapping', oldUri, newUri, '...'); + + // Ensure that oldLink has a unique ID so we can remove all stale stylesheets from the DOM after newLink loads. + // This is a more robust approach than holding a reference to oldLink and removing it directly, because a user might + // quickly switch themes several times before the first stylesheet finishes loading (especially over a slow network) + // and each new stylesheet would try to remove the first one, leaving multiple conflicting stylesheets in the DOM. + if (!oldLink.id) { + oldLink.id = `stylesheet-${Math.floor(Math.random() * Date.now())}`; + } + + const newLink = /** @type {!Element} */ (oldLink.cloneNode(false)); + newLink.setAttribute('href', newUri); + newLink.setAttribute(attrs.IS_LOADING, 'true'); + + // IE 11 and Edge fire the `load` event twice for `` elements. + newLink.addEventListener('load', util.debounce(() => { + this.handleStylesheetLoad_(newLink, newUri, oldUri); + }, 50)); + + oldLink.parentNode.insertBefore(newLink, oldLink); + + this.pendingRequests_.push(newUri); + this.toolbarProvider_.setIsLoading(true); + } + + /** + * @param {!Element} newLink + * @param {string} newUri + * @param {string} oldUri + * @private + */ + handleStylesheetLoad_(newLink, newUri, oldUri) { + this.pendingRequests_.splice(this.pendingRequests_.indexOf(newUri), 1); + if (this.pendingRequests_.length === 0) { + this.toolbarProvider_.setIsLoading(false); + } + + setTimeout(() => { + this.purgeOldStylesheets_(newLink); + + // Remove the 'loading' attribute *after* purging old stylesheets to avoid purging this one. + newLink.removeAttribute(attrs.IS_LOADING); + + this.logHotSwap_('swapped', oldUri, newUri, '!'); + }); + } + + /** + * @param {!Element} newLink + * @private + */ + purgeOldStylesheets_(newLink) { + let oldLinks; + + const getOldLinks = () => this.querySelectorAll_(`link[id="${newLink.id}"]:not([${attrs.IS_LOADING}])`); + + while ((oldLinks = getOldLinks()).length > 0) { + oldLinks.forEach((oldLink) => { + // Link has already been detached from the DOM. I'm not sure what causes this to happen; I've only seen it in + // IE 11 and/or Edge so far, and only occasionally. + if (!oldLink.parentNode) { + return; + } + oldLink.parentNode.removeChild(oldLink); + }); + } + } + + /** + * Adds a timestamp to the given URI to force IE 11 and Edge to bypass the cache and request a fresh copy of the CSS. + * @param oldUri + * @return {string} + * @private + */ + bustCache_(oldUri) { + const newUri = oldUri + // Remove previous timestamp param (if present) + .replace(/[?&]timestamp=\d+(&|$)/, '') + // Remove trailing '?' or '&' char (if present) + .replace(/[?&]$/, ''); + const separator = newUri.indexOf('?') === -1 ? '?' : '&'; + return `${newUri}${separator}timestamp=${Date.now()}`; + } + + /** + * @param {string} verb + * @param {string} oldUri + * @param {string} newUri + * @param {string} trailingPunctuation + * @private + */ + logHotSwap_(verb, oldUri, newUri, trailingPunctuation) { + const swapMessage = `"${oldUri}"${newUri ? ` with "${newUri}"` : ''}`; + console.log(`Hot ${verb} stylesheet ${swapMessage}${trailingPunctuation}`); + } + + /** + * @param {!Document|!Element} root + * @return {!HotSwapper} + */ + static getInstance(root) { + // Yeah, I know, this is gross. + if (!root.demoHotSwapperRootMap_) { + /** @type {?Map<{key:*, value:*}>} @private */ + root.demoHotSwapperRootMap_ = new Map(); + } + let instance = root.demoHotSwapperRootMap_.get(root); + if (!instance) { + instance = HotSwapper.attachTo(root, ToolbarProvider.attachTo(root)); + root.demoHotSwapperRootMap_.set(root, instance); + } + return instance; + } +} + +/** @param {!Document|!Element} root */ +export function init(root) { + HotSwapper.getInstance(root); +} diff --git a/demos/ponyfill.js b/demos/ponyfill.js new file mode 100644 index 00000000000..3f4689dc32a --- /dev/null +++ b/demos/ponyfill.js @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Object ponyfills from TC39 proposal (MIT license). + * https://github.com/tc39/proposal-object-values-entries/blob/7c2a54c56a529af1925a881e1b4c9e8b2d885a6c/polyfill.js + */ + +const reduce = Function.bind.call(Function.call, Array.prototype.reduce); +const isEnumerable = Function.bind.call(Function.call, Object.prototype.propertyIsEnumerable); +const concat = Function.bind.call(Function.call, Array.prototype.concat); +const keys = Object.keys; + +/** + * @param {!Object} obj + * @return {!Array<*>} An array of [key, value] pairs. + */ +export function objectValues(obj) { + return reduce(keys(obj), (v, k) => concat(v, typeof k === 'string' && isEnumerable(obj, k) ? [obj[k]] : []), []); +} + +/** + * @param {!Object} obj + * @return {!Array>} An array of [key, value] pairs. + */ +export function objectEntries(obj) { + return reduce(keys(obj), (e, k) => concat(e, typeof k === 'string' && isEnumerable(obj, k) ? [[k, obj[k]]] : []), []); +} + +/* + * DOM ponyfills + */ + +/** + * @param {!Element} elem + * @param {string} selector + * @return {boolean} + */ +export function matches(elem, selector) { + const nativeMatches = elem.matches + || elem.webkitMatchesSelector + || elem.mozMatchesSelector + || elem.msMatchesSelector + || elem.oMatchesSelector; + return nativeMatches.call(elem, selector); +} + +/** + * @param {!Element} elem + * @param {string} selector + * @return {?Element} + */ +export function closest(elem, selector) { + if (elem.closest) { + return elem.closest(selector); + } + while (elem) { + if (matches(elem, selector)) { + return elem; + } + elem = elem.parentElement; + } + return null; +} diff --git a/demos/ready.js b/demos/ready.js index e48e1a11899..dd617936bf3 100644 --- a/demos/ready.js +++ b/demos/ready.js @@ -20,9 +20,9 @@ * have finished loading (as determined by continuous long-polling with a timeout). If this function is called after all * resources have finished loading, the given handler function will be invoked synchronously (in the same call stack). * Handlers are invoked in FIFO order. - * @param {function() : undefined} handler + * @param {function(!Document|!Element) : undefined} handler */ -window.demoReady = (function() { +window.demoReady = (function(root) { var POLL_INTERVAL_MS = 100; var POLL_MAX_WAIT_MS = 60 * 1000; @@ -85,16 +85,16 @@ window.demoReady = (function() { function invokeHandlers() { handlers.forEach(function(handler) { - handler(); + handler(root); }); } return function addHandler(handler) { if (isReady()) { - handler(); + handler(root); return; } handlers.push(handler); startTimer(); }; -})(); +})(document); diff --git a/demos/theme-loader.js b/demos/theme-loader.js index 4221453b3a8..ec7eb497ca3 100644 --- a/demos/theme-loader.js +++ b/demos/theme-loader.js @@ -57,11 +57,11 @@ }; /** - * @param {string|undefined=} opt_uri + * @param {string=} uri * @return {!SafeTheme} */ - window.getSafeDemoThemeFromUri = function(opt_uri) { - return getSafeDemoTheme(parseQueryString(opt_uri).getLast('theme')); + window.getSafeDemoThemeFromUri = function(uri) { + return getSafeDemoTheme(parseQueryString(uri).getLast('theme')); }; /** @@ -79,10 +79,10 @@ }; /** @type {string} */ - var unwrappedTheme = unwrapSafeDemoTheme(getSafeDemoThemeFromUri()); + var unwrappedThemeStr = unwrapSafeDemoTheme(getSafeDemoThemeFromUri()); // Avoid a FOUC by injecting the stylesheet directly into the HTML stream while the browser is parsing the page. // This causes the browser to block page rendering until the CSS has finished loading. document.write( - ''); + ''); })(); diff --git a/demos/theme/_menu.scss b/demos/theme/_menu.scss new file mode 100644 index 00000000000..06b95c1a13b --- /dev/null +++ b/demos/theme/_menu.scss @@ -0,0 +1,66 @@ +// +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +@import "../../packages/mdc-theme/color-palette"; +@import "../../packages/mdc-theme/mixins"; + +// +// Toolbar and menu +// + +.demo-theme-menu__list-item--selected { + background-color: $material-color-grey-400; +} + +.demo-theme-list { + @include mdc-theme-prop(color, text-primary-on-light); +} + +// +// Radios +// + +.demo-theme-color-radio { + display: block; + border: 1px solid $material-color-grey-500; + border-radius: 50%; +} + +.demo-theme-color-radio__inner { + display: block; + border: 11px solid; + border-radius: 50%; +} + +[data-theme="baseline"] .demo-theme-color-radio__inner { + border-color: $material-color-deep-purple-a700 $material-color-teal-a400 $material-color-teal-a400 $material-color-deep-purple-a700; +} + +[data-theme="black"] .demo-theme-color-radio__inner { + border-color: $material-color-grey-900 $material-color-light-green-a700 $material-color-light-green-a700 $material-color-grey-900; +} + +[data-theme="dark"] .demo-theme-color-radio__inner { + border-color: $material-color-amber-300 $material-color-pink-400 $material-color-pink-400 $material-color-amber-300; +} + +[data-theme="white"] .demo-theme-color-radio__inner { + border-color: #fff $material-color-orange-500 $material-color-orange-500 #fff; +} + +[data-theme="yellow"] .demo-theme-color-radio__inner { + border-color: $material-color-yellow-500 $material-color-blue-500 $material-color-blue-500 $material-color-yellow-500; +} diff --git a/demos/theme/_theme-shared.scss b/demos/theme/_theme-shared.scss index 018b8f778d1..32547b25b4b 100644 --- a/demos/theme/_theme-shared.scss +++ b/demos/theme/_theme-shared.scss @@ -14,6 +14,7 @@ // limitations under the License. // +@import "../../packages/mdc-animation/functions"; @import "../../packages/mdc-button/mdc-button"; @import "../../packages/mdc-card/mdc-card"; @import "../../packages/mdc-checkbox/mdc-checkbox"; @@ -38,9 +39,13 @@ @import "../../packages/mdc-theme/variables"; @import "../../packages/mdc-toolbar/variables"; @import "../common"; +@import "./menu"; // stylelint-disable selector-class-pattern, selector-max-type, scss/dollar-variable-pattern +$demo-toolbar-progress-bar-color: secondary !default; +$demo-toolbar-progress-buffer-color: primary !default; + $demo-section-margin: 48px; $demo-color-custom: $material-color-red-500; @@ -51,20 +56,40 @@ $demo-color-high-luminance: $material-color-amber-50; // Global styles // -figure { - margin-left: 0; - margin-right: 0; -} - body { color: mdc-theme-accessible-ink-color($mdc-theme-background); background-color: $mdc-theme-background; } +figure { + margin-left: 0; + margin-right: 0; +} + // // Toolbar // +.demo-toolbar-progress-row { + min-height: auto; + overflow: hidden; + position: absolute; + bottom: 0; +} + +.demo-toolbar-progress-bar { + transform: translateY(100%); + transition: mdc-animation-exit-temporary(transform, 150ms); + + @include mdc-linear-progress-bar-color($demo-toolbar-progress-bar-color); + @include mdc-linear-progress-buffer-color($demo-toolbar-progress-buffer-color); +} + +.demo-toolbar-progress-bar--active { + transform: none; + transition: mdc-animation-enter(transform, 200ms); +} + .demo-anchor-with-toolbar-offset { $margin: $demo-section-margin / 2; @@ -133,6 +158,19 @@ body { // CSS class demo // +.demo-theme-color-section + .demo-theme-color-section { + margin-top: 36px; +} + +.demo-theme-color-section__row { + display: flex; + flex-wrap: wrap; +} + +.demo-fieldset--color { + margin-right: 36px; +} + .demo-theme-color-group { padding: 16px 0; } @@ -140,7 +178,7 @@ body { .demo-theme-color-swatches { display: inline-flex; flex-direction: column; - margin-right: 36px; + margin-right: 16px; } .demo-theme-color-swatch { @@ -248,12 +286,6 @@ body { margin-left: 56px; } -[dir="rtl"] .demo-card--with-avatar .mdc-card__title, -[dir="rtl"] .demo-card--with-avatar .mdc-card__subtitle { - margin-right: 56px; - margin-left: initial; -} - .demo-card--small .mdc-card__media { height: 10.938rem; // 175sp, for 1:1 ratio with 175sp demo card width background-image: url(/images/1-1.jpg); @@ -381,6 +413,18 @@ body { margin: 16px 16px 0 0; } +// +// Tab Bar demo +// + +.demo-figure--tab-bar + .demo-figure--tab-bar { + margin-top: 36px; +} + +.demo-tab-bar { + margin: 0; +} + // // Text Field demo // @@ -389,3 +433,7 @@ body { display: inline-block; margin: 16px 16px 0 0; } + +.demo-text-field { + margin-top: 0 !important; +} diff --git a/demos/theme/index.html b/demos/theme/index.html index 2387b1e968a..27512c5f499 100644 --- a/demos/theme/index.html +++ b/demos/theme/index.html @@ -31,12 +31,82 @@ -
+
Color Theming
+ +
+
+
+
+
+
+ +
+
+ +
+
@@ -92,129 +162,143 @@

#

-
- Theme colors as text - -
-
-
Primary
-
Primary Light
-
Primary Dark
-
-
-
Secondary
-
Secondary Light
-
Secondary Dark
-
-
-
- -
- Theme colors as background - -
-
-
Primary
-
Primary Light
-
Primary Dark
-
-
-
Secondary
-
Secondary Light
-
Secondary Dark
-
-
-
Background
-
-
-
- -
- Light and dark tonal variants for extreme input luminance - -
-
-
Low Luminance
-
Light Variant
-
Dark Variant
-
-
-
High Luminance
-
Light Variant
-
Dark Variant
-
-
-
- -
- Text on background - -
-
- Primary - Secondary - Hint - Disabled - favorite -
-
-
- -
- Text on primary - -
-
- Primary - Secondary - Hint - Disabled - favorite -
+
+
+
+ Theme colors as text + +
+
+
Primary
+
Primary Light
+
Primary Dark
+
+
+
Secondary
+
Secondary Light
+
Secondary Dark
+
+
+
+ +
+ Theme colors as background + +
+
+
Primary
+
Primary Light
+
Primary Dark
+
+
+
Secondary
+
Secondary Light
+
Secondary Dark
+
+
+
Background
+
+
+
+
+ +
+
+ Light and dark tonal variants for extreme input luminance + +
+
+
Low Luminance
+
Light Variant
+
Dark Variant
+
+
+
High Luminance
+
Light Variant
+
Dark Variant
+
+
+
-
- -
- Text on secondary + -
-
- Primary - Secondary - Hint - Disabled - favorite -
+
+
+
+ Text on background + +
+
+ Primary + Secondary + Hint + Disabled + favorite +
+
+
-
-
- Text on user-defined light background +
+
+ Text on primary -
-
- Primary - Secondary - Hint - Disabled - favorite -
+
+
+ Primary + Secondary + Hint + Disabled + favorite +
+
+
+ +
+ Text on secondary + +
+
+ Primary + Secondary + Hint + Disabled + favorite +
+
+
-
-
- Text on user-defined dark background +
+
+ Text on user-defined light background -
-
- Primary - Secondary - Hint - Disabled - favorite -
+
+
+ Primary + Secondary + Hint + Disabled + favorite +
+
+
+ +
+ Text on user-defined dark background + +
+
+ Primary + Secondary + Hint + Disabled + favorite +
+
+
-
+

@@ -691,7 +775,7 @@

#

-
+
Default
-
+
Primary Color
-
+
Secondary Color