diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-12-10 16:19:40 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-12-10 16:01:50 +0000 |
commit | 51f6c2793adab2d864b3d2b360000ef8db1d3e92 (patch) | |
tree | 835b3b4446b012c75e80177cef9fbe6972cc7dbe /chromium/ui/webui | |
parent | 6036726eb981b6c4b42047513b9d3f4ac865daac (diff) | |
download | qtwebengine-chromium-51f6c2793adab2d864b3d2b360000ef8db1d3e92.tar.gz |
BASELINE: Update Chromium to 71.0.3578.93
Change-Id: I6a32086c33670e1b033f8b10e6bf1fd4da1d105d
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Diffstat (limited to 'chromium/ui/webui')
81 files changed, 3859 insertions, 449 deletions
diff --git a/chromium/ui/webui/PLATFORM_OWNERS b/chromium/ui/webui/PLATFORM_OWNERS index 29e5a4ae960..a93051bd3f8 100644 --- a/chromium/ui/webui/PLATFORM_OWNERS +++ b/chromium/ui/webui/PLATFORM_OWNERS @@ -1,10 +1,8 @@ # Please use more specific OWNERS when possible. -bauerb@chromium.org calamity@chromium.org dpapad@chromium.org dschuyler@chromium.org michaelpg@chromium.org -pam@chromium.org scottchen@chromium.org stevenjb@chromium.org tommycli@chromium.org diff --git a/chromium/ui/webui/mojo_web_ui_controller.cc b/chromium/ui/webui/mojo_web_ui_controller.cc index e2de8d20f8a..769e194449c 100644 --- a/chromium/ui/webui/mojo_web_ui_controller.cc +++ b/chromium/ui/webui/mojo_web_ui_controller.cc @@ -24,6 +24,12 @@ void MojoWebUIController::OnInterfaceRequestFromFrame( content::RenderFrameHost* render_frame_host, const std::string& interface_name, mojo::ScopedMessagePipeHandle* interface_pipe) { + if (!registry_.CanBindInterface(interface_name)) { + LOG(WARNING) << "Cannot bind request to " << interface_name << "; ignoring " + << "request."; + return; + } + // Right now, this is expected to be called only for main frames. if (render_frame_host->GetParent()) { LOG(ERROR) << "Terminating renderer for requesting " << interface_name diff --git a/chromium/ui/webui/resources/cr_components/BUILD.gn b/chromium/ui/webui/resources/cr_components/BUILD.gn index f3d71a3a372..b39708e6607 100644 --- a/chromium/ui/webui/resources/cr_components/BUILD.gn +++ b/chromium/ui/webui/resources/cr_components/BUILD.gn @@ -7,6 +7,9 @@ import("//third_party/closure_compiler/compile_js.gni") group("closure_compile") { deps = [ "certificate_manager:closure_compile", - "chromeos:closure_compile", ] + + if (is_chromeos) { + deps += [ "chromeos:closure_compile" ] + } } diff --git a/chromium/ui/webui/resources/cr_components/chromeos/BUILD.gn b/chromium/ui/webui/resources/cr_components/chromeos/BUILD.gn index 0fd1a54821e..3c7e36300ab 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/BUILD.gn +++ b/chromium/ui/webui/resources/cr_components/chromeos/BUILD.gn @@ -4,9 +4,12 @@ import("//third_party/closure_compiler/compile_js.gni") +assert(is_chromeos, "Only ChromeOS components belong here.") + group("closure_compile") { deps = [ ":chromeos_resources", + "multidevice_setup:closure_compile", "network:closure_compile", "quick_unlock:closure_compile", ] diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/.eslintrc.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/.eslintrc.js new file mode 100644 index 00000000000..25e21f992eb --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/.eslintrc.js @@ -0,0 +1,13 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +module.exports = { + 'env': { + 'browser': true, + 'es6': true, + }, + 'rules': { + 'no-var': 'error', + }, +}; diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/BUILD.gn b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/BUILD.gn new file mode 100644 index 00000000000..616ad6e48f5 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/BUILD.gn @@ -0,0 +1,129 @@ +# Copyright 2018 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//third_party/closure_compiler/compile_js.gni") + +assert(is_chromeos, "MultiDevice UI is Chrome OS only.") + +js_type_check("closure_compile") { + deps = [ + ":button_bar", + ":fake_mojo_service", + ":mojo_api", + ":multidevice_setup", + ":multidevice_setup_browser_proxy", + ":multidevice_setup_delegate", + ":setup_failed_page", + ":setup_succeeded_page", + ":start_setup_page", + ":ui_page_container_behavior", + "//ui/webui/resources/js:web_ui_listener_behavior", + ] +} + +js_library("button_bar") { +} + +js_library("fake_mojo_service") { + deps = [ + "//ui/webui/resources/js:cr", + ] + + extra_deps = [ + "//chromeos/services/device_sync/public/mojom:mojom_js", + "//chromeos/services/multidevice_setup/public/mojom:mojom_js", + "//mojo/public/mojom/base:base_js", + ] + + externs_list = [ + "$root_gen_dir/chromeos/services/device_sync/public/mojom/device_sync.mojom.externs.js", + "$root_gen_dir/chromeos/services/multidevice_setup/public/mojom/multidevice_setup.mojom.externs.js", + "$root_gen_dir/mojo/public/mojom/base/time.mojom.externs.js", + "$externs_path/mojo.js", + ] +} + +js_library("mojo_api") { + deps = [ + "//ui/webui/resources/js:cr", + ] +} + +js_library("multidevice_setup") { + deps = [ + ":button_bar", + ":fake_mojo_service", + ":mojo_api", + ":multidevice_setup_delegate", + ":password_page", + ":setup_failed_page", + ":setup_succeeded_page", + ":start_setup_page", + "//ui/webui/resources/js:cr", + "//ui/webui/resources/js:web_ui_listener_behavior", + ] + + extra_deps = [ + "//chromeos/services/device_sync/public/mojom:mojom_js", + "//chromeos/services/multidevice_setup/public/mojom:mojom_js", + "//mojo/public/mojom/base:base_js", + ] + + externs_list = [ + "$root_gen_dir/chromeos/services/device_sync/public/mojom/device_sync.mojom.externs.js", + "$root_gen_dir/chromeos/services/multidevice_setup/public/mojom/multidevice_setup.mojom.externs.js", + "$root_gen_dir/mojo/public/mojom/base/time.mojom.externs.js", + "$externs_path/mojo.js", + ] +} + +js_library("multidevice_setup_browser_proxy") { + deps = [ + "//ui/webui/resources/js:cr", + ] +} + +js_library("multidevice_setup_delegate") { + deps = [ + "//ui/webui/resources/js:cr", + ] +} + +js_library("password_page") { + deps = [ + ":multidevice_setup_browser_proxy", + ":ui_page_container_behavior", + "//ui/webui/resources/cr_elements/cr_input:cr_input", + "//ui/webui/resources/js:cr", + ] + externs_list = [ "$externs_path/quick_unlock_private.js" ] + extra_sources = [ "$interfaces_path/quick_unlock_private_interface.js" ] +} + +js_library("setup_failed_page") { + deps = [ + ":ui_page_container_behavior", + ] +} + +js_library("setup_succeeded_page") { + deps = [ + ":multidevice_setup_browser_proxy", + ":ui_page_container_behavior", + ] +} + +js_library("start_setup_page") { + deps = [ + ":ui_page_container_behavior", + "//ui/webui/resources/js:web_ui_listener_behavior", + ] +} + +js_library("ui_page_container_behavior") { + deps = [ + "//ui/webui/resources/js:cr", + "//ui/webui/resources/js:i18n_behavior", + ] +} diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/OWNERS b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/OWNERS new file mode 100644 index 00000000000..aef4ecb9195 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/OWNERS @@ -0,0 +1 @@ +file://chromeos/services/multidevice_setup/OWNERS diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.html new file mode 100644 index 00000000000..7b6c70c4df6 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.html @@ -0,0 +1,31 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> + +<dom-module id="button-bar"> + <template> + <style include="multidevice-setup-shared"> + :host { + display: flex; + } + </style> + <div id="backward" + on-click="onBackwardButtonClicked_" + hidden$="[[backwardButtonHidden]]"> + <slot name="backward-button"></slot> + </div> + <div class="flex"></div> + <div id="cancel" + on-click="onCancelButtonClicked_" + hidden$="[[cancelButtonHidden]]"> + <slot name="cancel-button"></slot> + </div> + <div id="forward" + on-click="onForwardButtonClicked_" + hidden$="[[forwardButtonHidden]]"> + <slot name="forward-button"></slot> + </div> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/button_bar.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.js new file mode 100644 index 00000000000..f556598dce9 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/button_bar.js @@ -0,0 +1,46 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * DOM Element containing (page-dependent) navigation buttons for the + * MultiDevice Setup WebUI. + */ +Polymer({ + is: 'button-bar', + + properties: { + /** Whether the forward button should be hidden. */ + forwardButtonHidden: { + type: Boolean, + value: true, + }, + + /** Whether the cancel button should be hidden. */ + cancelButtonHidden: { + type: Boolean, + value: true, + }, + + /** Whether the backward button should be hidden. */ + backwardButtonHidden: { + type: Boolean, + value: true, + }, + }, + + /** @private */ + onForwardButtonClicked_: function() { + this.fire('forward-navigation-requested'); + }, + + /** @private */ + onCancelButtonClicked_: function() { + this.fire('cancel-requested'); + }, + + /** @private */ + onBackwardButtonClicked_: function() { + this.fire('backward-navigation-requested'); + }, +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.html new file mode 100644 index 00000000000..12556f5782b --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.html @@ -0,0 +1,4 @@ +<link rel="import" href="chrome://resources/html/cr.html"> + +<script src="chrome://resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.js"> +</script> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.js new file mode 100644 index 00000000000..48e3eaa4ef0 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.js @@ -0,0 +1,108 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @implements {chromeos.multideviceSetup.mojom.MultiDeviceSetupImpl} + */ +class FakeMojoService { + constructor() { + /** + * The number of devices to return in a getEligibleHostDevices() call. + * @type {number} + */ + this.deviceCount = 2; + + /** + * Whether calls to setHostDevice() should succeed. + * @type {boolean} + */ + this.shouldSetHostSucceed = true; + } + + /** @override */ + setAccountStatusChangeDelegate(delegate) { + // Unimplemented; never called from setup flow. + assertNotReached(); + } + + /** @override */ + addHostStatusObserver(observer) { + // Unimplemented; never called from setup flow. + assertNotReached(); + } + + /** @override */ + addFeatureStateObserver(observer) { + // Unimplemented; never called from setup flow. + assertNotReached(); + } + + /** @override */ + getEligibleHostDevices() { + const deviceNames = ['Pixel', 'Pixel XL', 'Nexus 5', 'Nexus 6P']; + let devices = []; + for (let i = 0; i < this.deviceCount; i++) { + const deviceName = deviceNames[i % 4]; + devices.push({deviceName: deviceName, deviceId: deviceName + '--' + i}); + } + return new Promise(function(resolve, reject) { + resolve({eligibleHostDevices: devices}); + }); + } + + /** @override */ + setHostDevice(deviceId) { + if (this.shouldSetHostSucceed) { + console.log( + 'setHostDevice(' + deviceId + ') called; simulating ' + + 'success.'); + } else { + console.warn('setHostDevice() called; simulating failure.'); + } + return new Promise((resolve, reject) => { + resolve({success: this.shouldSetHostSucceed}); + }); + } + + /** @override */ + removeHostDevice() { + // Unimplemented; never called from setup flow. + assertNotReached(); + } + + /** @override */ + getHostStatus() { + return new Promise((resolve, reject) => { + reject('Unimplemented; never called from setup flow.'); + }); + } + + /** @override */ + setFeatureEnabledState() { + return new Promise((resolve, reject) => { + reject('Unimplemented; never called from setup flow.'); + }); + } + + /** @override */ + getFeatureStates() { + return new Promise((resolve, reject) => { + reject('Unimplemented; never called from setup flow.'); + }); + } + + /** @override */ + retrySetHostNow() { + return new Promise((resolve, reject) => { + reject('Unimplemented; never called from setup flow.'); + }); + } + + /** @override */ + triggerEventForDebugging(type) { + return new Promise((resolve, reject) => { + reject('Unimplemented; never called from setup flow.'); + }); + } +} diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/icons.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/icons.html new file mode 100644 index 00000000000..0232cf476e4 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/icons.html @@ -0,0 +1,36 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/polymer/v1_0/iron-iconset-svg/iron-iconset-svg.html"> + +<iron-iconset-svg name="multidevice-setup-icons-32" size="32"> + <svg> + <defs> + <g id="google-g" fill="none" fill-rule="evenodd"> + <path d="M30.42 16.83a16.66 16.66 0 0 0-.264-2.966H16.5v5.609h7.804c-.336 1.812-1.358 3.348-2.894 4.376v3.638h4.686c2.742-2.524 4.324-6.242 4.324-10.657z" fill="#4285F4"></path> + <path d="M16.5 31c3.915 0 7.197-1.298 9.596-3.513l-4.686-3.638c-1.298.87-2.96 1.384-4.91 1.384-3.777 0-6.973-2.55-8.113-5.978H3.542v3.757C5.928 27.75 10.832 31 16.5 31z" fill="#34A853"></path> + <path d="M8.387 19.255c-.29-.87-.455-1.8-.455-2.755 0-.956.165-1.885.455-2.755V9.988H3.542A14.494 14.494 0 0 0 2 16.5c0 2.34.56 4.554 1.542 6.512l4.845-3.757z" fill="#FBBC05"></path> + <path d="M16.5 7.767c2.129 0 4.04.732 5.543 2.168l4.159-4.158C23.69 3.437 20.408 2 16.5 2 10.832 2 5.928 5.25 3.542 9.988l4.845 3.757c1.14-3.427 4.336-5.978 8.113-5.978z" fill="#EA4335"></path> + <path d="M2 2h29v29H2z"></path> + </g> + <g id="error-icon" fill="none" fill-rule="evenodd" transform="translate(-4 -4)"> + <path d="M20 4C11.168 4 4 11.168 4 20s7.168 16 16 16 16-7.168 16-16S28.832 4 20 4zm1.6 24h-3.2v-3.2h3.2V28zm0-6.4h-3.2V12h3.2v9.6z" fill="#D93025" fill-rule="nonzero"></path> + <path d="M.8.8h38.4v38.4H.8z"></path> + </g> + </defs> + </svg> +</iron-iconset-svg> +<iron-iconset-svg name="multidevice-setup-icons-20" size="20"> + <svg> + <defs> + <g id="messages" fill="none" fill-rule="evenodd"> + <path d="M16.3107503,3 L3.66666667,3 C2.75,3 2,3.75 2,4.66666667 L2,18.3161621 L5.33333333,15 L16.3107503,15 C17.227417,15 17.977417,14.2328288 17.977417,13.3161621 L17.977417,4.66666667 C17.977417,3.75 17.227417,3 16.3107503,3 Z M16,13 L4,13 L4,5 L16,5 L16,13 Z M6,8 L8,8 L8,10 L6,10 L6,8 Z M9,8 L11,8 L11,10 L9,10 L9,8 Z M12,8 L14,8 L14,10 L12,10 L12,8 Z" fill="#9AA0A6"></path> + </g> + <g id="downloads" fill="none" fill-rule="evenodd"> + <path d="M2,13 L4,13 L4,16 L16,16 L16,13 L18,13 L18,16 C18,17.1 17.1,18 16,18 L4,18 C2.9,18 2,17.1 2,16 L2,13 Z M13.59,7.59 L11,10.17 L11,2 L9,2 L9,10.17 L6.41,7.59 L5,9 L10,14 L15,9 L13.59,7.59 Z" fill="#9AA0A6"></path> + </g> + <g id="features" fill="none" fill-rule="evenodd"> + <path d="M5,5 L18,5 L18,3.5 L5.16080729,3.5 C4.24414063,3.5 3.49414062,4.23125 3.49414062,5.125 L3.49414062,14 L1,14 L1,17 L11,17 L11,14 L5,14 L5,5 Z M18.1666667,6.49829102 L13.3713582,6.49829102 C12.9130249,6.49829102 12.5,6.86391602 12.5,7.31079102 L12.5,16.171875 C12.5,16.61875 12.9130249,17 13.3713582,17 L18.1666667,17 C18.625,17 19,16.61875 19,16.171875 L19,7.31079102 C19,6.86391602 18.625,6.49829102 18.1666667,6.49829102 Z M17.5,14 L14,14 L14,8.5 L17.5,8.5 L17.5,14 Z" fill="#9AA0A6"></path> + </g> + </defs> + </svg> +</iron-iconset-svg> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.html new file mode 100644 index 00000000000..2bcb1bbc67d --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.html @@ -0,0 +1,10 @@ +<link rel="import" href="chrome://resources/html/cr.html"> +<script src="chrome://resources/js/mojo_bindings.js"></script> +<script src="chrome://resources/js/time.mojom.js"></script> +<script src="chrome://resources/js/chromeos/device_sync.mojom.js"></script> +<script src="chrome://resources/js/chromeos/multidevice_setup.mojom.js"> +</script> +<script src="chrome://resources/js/chromeos/multidevice_setup_constants.mojom.js"> +</script> +<script src="chrome://resources/cr_components/chromeos/multidevice_setup/mojo_api.js"> +</script> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.js new file mode 100644 index 00000000000..d2aba99703f --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/mojo_api.js @@ -0,0 +1,40 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('multidevice_setup', function() { + /** @interface */ + class MojoInterfaceProvider { + /** + * @return {!chromeos.multideviceSetup.mojom.MultiDeviceSetupImpl} + */ + getInterfacePtr() {} + } + + /** @implements {multidevice_setup.MojoInterfaceProvider} */ + class MojoInterfaceProviderImpl { + constructor() { + /** @private {?chromeos.multideviceSetup.mojom.MultiDeviceSetupPtr} */ + this.ptr_ = null; + } + + /** @override */ + getInterfacePtr() { + if (!this.ptr_) { + this.ptr_ = new chromeos.multideviceSetup.mojom.MultiDeviceSetupPtr(); + Mojo.bindInterface( + chromeos.multideviceSetup.mojom.MultiDeviceSetup.name, + mojo.makeRequest(this.ptr_).handle); + } + + return this.ptr_; + } + } + + cr.addSingletonGetter(MojoInterfaceProviderImpl); + + return { + MojoInterfaceProvider: MojoInterfaceProvider, + MojoInterfaceProviderImpl: MojoInterfaceProviderImpl, + }; +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.html new file mode 100644 index 00000000000..a75218ee419 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.html @@ -0,0 +1,67 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/button_bar.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/fake_mojo_service.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/mojo_api.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/password_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/setup_failed_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/start_setup_page.html"> +<link rel="import" href="chrome://resources/html/cr.html"> +<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/iron-pages/iron-pages.html"> + +<dom-module id="multidevice-setup"> + <template> + <style include="multidevice-setup-shared"> + :host { + @apply --layout-vertical; + box-sizing: border-box; + color: var(--google-grey-700); + font-size: 13px; + height: 640px; + line-height: 16px; + margin: auto; + padding: 60px 32px 32px 32px; + width: 768px; + } + + iron-pages { + padding: 0 32px; + } + </style> + <iron-pages attr-for-selected="is" + selected="[[visiblePageName_]]" + selected-item="{{visiblePage_}}"> + <template is="dom-if" if="[[shouldPasswordPageBeIncluded_(delegate)]]" + restamp> + <password-page auth-token="{{authToken_}}" + forward-button-disabled="{{passwordPageForwardButtonDisabled_}}" + password-field-valid="{{passwordFieldValid}}" + on-user-submitted-password="onUserSubmittedPassword_"> + </password-page> + </template> + <setup-failed-page></setup-failed-page> + <template is="dom-if" + if="[[shouldSetupSucceededPageBeIncluded_(delegate)]]" restamp> + <setup-succeeded-page></setup-succeeded-page> + </template> + <start-setup-page devices="[[devices_]]" + selected-device-id="{{selectedDeviceId_}}" + delegate="[[delegate]]"> + </start-setup-page> + </iron-pages> + <div class="flex"></div> + <button-bar forward-button-hidden="[[!forwardButtonText]]" + backward-button-hidden="[[!backwardButtonText]]" + cancel-button-hidden="[[!cancelButtonText]]"> + <slot name="backward-button" slot="backward-button"></slot> + <slot name="cancel-button" slot="cancel-button"></slot> + <slot name="forward-button" slot="forward-button"></slot> + </button-bar> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.js new file mode 100644 index 00000000000..3dc0dd457d8 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup.js @@ -0,0 +1,312 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.exportPath('multidevice_setup'); + +/** @enum {string} */ +multidevice_setup.PageName = { + FAILURE: 'setup-failed-page', + PASSWORD: 'password-page', + SUCCESS: 'setup-succeeded-page', + START: 'start-setup-page', +}; + +cr.define('multidevice_setup', function() { + const PageName = multidevice_setup.PageName; + + const MultiDeviceSetup = Polymer({ + is: 'multidevice-setup', + + behaviors: [WebUIListenerBehavior], + + properties: { + /** + * Delegate object which performs differently in OOBE vs. non-OOBE mode. + * @type {!multidevice_setup.MultiDeviceSetupDelegate} + */ + delegate: Object, + + /** + * Text to be shown on the forward navigation button. + * @type {string|undefined} + */ + forwardButtonText: { + type: String, + computed: 'getForwardButtonText_(visiblePage_)', + notify: true, + }, + + /** Whether the forward button should be disabled. */ + forwardButtonDisabled: { + type: Boolean, + computed: 'shouldForwardButtonBeDisabled_(' + + 'passwordPageForwardButtonDisabled_, visiblePageName_)', + notify: true + }, + + /** + * Text to be shown on the cancel button. + * @type {string|undefined} + */ + cancelButtonText: { + type: String, + computed: 'getCancelButtonText_(visiblePage_)', + notify: true, + }, + + /** + * Text to be shown on the backward navigation button. + * @type {string|undefined} + */ + backwardButtonText: { + type: String, + computed: 'getBackwardButtonText_(visiblePage_)', + notify: true, + }, + + /** + * Element name of the currently visible page. + * + * @private {!multidevice_setup.PageName} + */ + visiblePageName_: { + type: String, + value: PageName.START, + notify: true, // For testing purposes only. + }, + + /** + * DOM Element corresponding to the visible page. + * + * @private {!PasswordPageElement|!StartSetupPageElement| + * !SetupSucceededPageElement|!SetupFailedPageElement} + */ + visiblePage_: Object, + + /** + * Authentication token, which is generated by the password page. + * @private {string} + */ + authToken_: { + type: String, + }, + + /** + * Array of objects representing all potential MultiDevice hosts. + * + * @private {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} + */ + devices_: Array, + + /** + * Unique identifier for the currently selected host device. + * + * Undefined if the no list of potential hosts has been received from mojo + * service. + * + * @private {string|undefined} + */ + selectedDeviceId_: String, + + /** + * Whether the password page reports that the forward button should be + * disabled. This field is only relevant when the password page is + * visible. + * @private {boolean} + */ + passwordPageForwardButtonDisabled_: Boolean, + + /** + * Provider of an interface to the MultiDeviceSetup Mojo service. + * @private {!multidevice_setup.MojoInterfaceProvider} + */ + mojoInterfaceProvider_: Object + }, + + listeners: { + 'backward-navigation-requested': 'onBackwardNavigationRequested_', + 'cancel-requested': 'onCancelRequested_', + 'forward-navigation-requested': 'onForwardNavigationRequested_', + }, + + /** @override */ + created: function() { + this.mojoInterfaceProvider_ = + multidevice_setup.MojoInterfaceProviderImpl.getInstance(); + }, + + /** @override */ + ready: function() { + this.addWebUIListener( + 'multidevice_setup.initializeSetupFlow', + this.initializeSetupFlow.bind(this)); + }, + + initializeSetupFlow: function() { + this.mojoInterfaceProvider_.getInterfacePtr() + .getEligibleHostDevices() + .then((responseParams) => { + if (responseParams.eligibleHostDevices.length == 0) { + console.warn('Potential host list is empty.'); + return; + } + + this.devices_ = responseParams.eligibleHostDevices; + }) + .catch((error) => { + console.warn('Mojo service failure: ' + error); + }); + }, + + /** @private */ + onCancelRequested_: function() { + this.exitSetupFlow_(); + }, + + /** @private */ + onBackwardNavigationRequested_: function() { + // The back button is only visible on the password page. + assert(this.visiblePageName_ == PageName.PASSWORD); + + this.$$('password-page').clearPasswordTextInput(); + this.visiblePageName_ = PageName.START; + }, + + /** @private */ + onForwardNavigationRequested_: function() { + if (this.forwardButtonDisabled) + return; + + this.visiblePage_.getCanNavigateToNextPage().then((canNavigate) => { + if (!canNavigate) + return; + this.navigateForward_(); + }); + }, + + /** @private */ + navigateForward_: function() { + switch (this.visiblePageName_) { + case PageName.FAILURE: + this.visiblePageName_ = PageName.START; + return; + case PageName.PASSWORD: + this.$$('password-page').clearPasswordTextInput(); + this.setHostDevice_(); + return; + case PageName.SUCCESS: + this.exitSetupFlow_(); + return; + case PageName.START: + if (this.delegate.isPasswordRequiredToSetHost()) + this.visiblePageName_ = PageName.PASSWORD; + else + this.setHostDevice_(); + return; + } + }, + + /** @private */ + setHostDevice_: function() { + // An authentication token must be set if a password is required. + assert(this.delegate.isPasswordRequiredToSetHost() == !!this.authToken_); + + let deviceId = /** @type {string} */ (this.selectedDeviceId_); + this.delegate.setHostDevice(deviceId, this.authToken_) + .then((responseParams) => { + if (!responseParams.success) { + console.warn('Failure setting host with device ID: ' + deviceId); + return; + } + + if (this.delegate.shouldExitSetupFlowAfterSettingHost()) { + this.exitSetupFlow_(); + return; + } + + this.visiblePageName_ = PageName.SUCCESS; + this.fire('forward-button-focus-requested'); + }) + .catch((error) => { + console.warn('Mojo service failure: ' + error); + }); + }, + + /** @private */ + onUserSubmittedPassword_: function() { + this.onForwardNavigationRequested_(); + }, + + /** + * @return {string|undefined} The forward button text, which is undefined + * if no button should be displayed. + * @private + */ + getForwardButtonText_: function() { + if (!this.visiblePage_) + return undefined; + return this.visiblePage_.forwardButtonText; + }, + + /** + * @return {boolean} Whether the forward button should be disabled. + * @private + */ + shouldForwardButtonBeDisabled_: function() { + return (this.visiblePageName_ == PageName.PASSWORD) && + this.passwordPageForwardButtonDisabled_; + }, + + /** + * @return {string|undefined} The cancel button text, which is undefined + * if no button should be displayed. + * @private + */ + getCancelButtonText_: function() { + if (!this.visiblePage_) + return undefined; + return this.visiblePage_.cancelButtonText; + }, + + /** + * @return {string|undefined} The backward button text, which is undefined + * if no button should be displayed. + * @private + */ + getBackwardButtonText_: function() { + if (!this.visiblePage_) + return undefined; + return this.visiblePage_.backwardButtonText; + }, + + /** + * @return {boolean} + * @private + */ + shouldPasswordPageBeIncluded_: function() { + return this.delegate.isPasswordRequiredToSetHost(); + }, + + /** + * @return {boolean} + * @private + */ + shouldSetupSucceededPageBeIncluded_: function() { + return !this.delegate.shouldExitSetupFlowAfterSettingHost(); + }, + + /** + * Notifies observers that the setup flow has completed. + * + * @private + */ + exitSetupFlow_: function() { + this.fire('setup-exited'); + }, + }); + + return { + MultiDeviceSetup: MultiDeviceSetup, + }; +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.html new file mode 100644 index 00000000000..82f8d961c25 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.html @@ -0,0 +1,3 @@ +<link rel="import" href="chrome://resources/html/cr.html"> +<script src="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.js"> +</script> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.js new file mode 100644 index 00000000000..be88d746277 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.js @@ -0,0 +1,41 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('multidevice_setup', function() { + /** @interface */ + class BrowserProxy { + /** + * Requests profile information; namely, a dictionary containing the user's + * e-mail address and profile photo. + * @return {!Promise<{profilePhotoUrl: string, email: string}>} + */ + getProfileInfo() {} + + /** + * Opens settings to the MultiDevice individual feature settings subpage. + * (a.k.a. Connected Devices). + */ + openMultiDeviceSettings() {} + } + + /** @implements {multidevice_setup.BrowserProxy} */ + class BrowserProxyImpl { + /** @override */ + getProfileInfo() { + return cr.sendWithPromise('getProfileInfo'); + } + + /** @override */ + openMultiDeviceSettings() { + chrome.send('openMultiDeviceSettings'); + } + } + + cr.addSingletonGetter(BrowserProxyImpl); + + return { + BrowserProxy: BrowserProxy, + BrowserProxyImpl: BrowserProxyImpl, + }; +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.html new file mode 100644 index 00000000000..46ce0b9a518 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.html @@ -0,0 +1,4 @@ +<link rel="import" href="chrome://resources/html/cr.html"> +<script src="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.js"> +</script> + diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.js new file mode 100644 index 00000000000..8526f38216b --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.js @@ -0,0 +1,33 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('multidevice_setup', function() { + /** + * Interface which provides the ability to set the host device and perform + * related logic. + * @interface + */ + class MultiDeviceSetupDelegate { + /** @return {boolean} */ + isPasswordRequiredToSetHost() {} + + /** + * @param {string} hostDeviceId The ID of the host to set. + * @param {string=} opt_authToken An auth token to authenticate the request; + * only necessary if isPasswordRequiredToSetHost() returns true. + * @return {!Promise<{success: boolean}>} + */ + setHostDevice(hostDeviceId, opt_authToken) {} + + /** @return {boolean} */ + shouldExitSetupFlowAfterSettingHost() {} + + /** @return {string} */ + getStartSetupCancelButtonTextId() {} + } + + return { + MultiDeviceSetupDelegate: MultiDeviceSetupDelegate, + }; +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html new file mode 100644 index 00000000000..c9403122d45 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html @@ -0,0 +1,19 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_elements/paper_button_style_css.html"> +<link rel="import" href="chrome://resources/cr_elements/shared_style_css.html"> +<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html"> +<link rel="import" href="chrome://resources/html/md_select_css.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout-classes.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout.html"> + +<dom-module id="multidevice-setup-shared"> + <template> + <style include="iron-flex paper-button-style cr-shared-style md-select"> + a { + color: var(--google-blue-600); + text-decoration: none; + } + </style> + </template> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.html new file mode 100644 index 00000000000..b5c52c0f573 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.html @@ -0,0 +1,51 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html"> +<link rel="import" href="chrome://resources/cr_elements/cr_input/cr_input.html"> +<link rel="import" href="chrome://resources/html/cr.html"> + +<dom-module id="password-page"> + <template> + <style include="multidevice-setup-shared"> + #user-info-container { + @apply --layout-horizontal; + align-items: center; + color: var(--paper-grey-600); + } + + #profile-photo { + border-radius: 50%; + height: 20px; + margin-right: 8px; + width: 20px; + } + + #passwordInput { + height: 32px; + margin-top: 64px; + width: 560px; + } + </style> + <ui-page header-text="[[headerText]]" icon-name="google-g"> + <div id="content-container" slot="additional-content"> + <div id="user-info-container"> + <img id="profile-photo" src="[[profilePhotoUrl_]]"></img> + <span id="email">[[email_]]</span> + </div> + <cr-input id="passwordInput" type="password" + placeholder="[[i18n('enterPassword')]]" + invalid="[[passwordInvalid_]]" + error-message="[[i18n('wrongPassword')]]" + value="{{inputValue_}}" + aria-disabled="false" + on-keypress="onInputKeypress_" + autofocus> + </cr-input> + </div> + </ui-page> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/password_page.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.js new file mode 100644 index 00000000000..ba2abe0bf6f --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/password_page.js @@ -0,0 +1,167 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +Polymer({ + is: 'password-page', + + behaviors: [ + UiPageContainerBehavior, + ], + + properties: { + /** + * Whether forward button should be disabled. In this context, the forward + * button should be disabled if the user has not entered a password or if + * the user has submitted an incorrect password and has not yet edited it. + * @type {boolean} + */ + forwardButtonDisabled: { + type: Boolean, + computed: 'shouldForwardButtonBeDisabled_(' + + 'inputValue_, passwordInvalid_, waitingForPasswordCheck_)', + notify: true, + }, + + /** Overridden from UiPageContainerBehavior. */ + forwardButtonTextId: { + type: String, + value: 'done', + }, + + /** Overridden from UiPageContainerBehavior. */ + cancelButtonTextId: { + type: String, + value: 'cancel', + }, + + /** Overridden from UiPageContainerBehavior. */ + backwardButtonTextId: { + type: String, + value: 'back', + }, + + /** Overridden from UiPageContainerBehavior. */ + headerId: { + type: String, + value: 'passwordPageHeader', + }, + + /** + * Authentication token; retrieved using the quickUnlockPrivate API. + * @type {string} + */ + authToken: { + type: String, + value: '', + notify: true, + }, + + /** @private {string} */ + profilePhotoUrl_: { + type: String, + value: '', + }, + + /** @private {string} */ + email_: { + type: String, + value: '', + }, + + /** @private {!QuickUnlockPrivate} */ + quickUnlockPrivate_: { + type: Object, + value: chrome.quickUnlockPrivate, + }, + + /** @private {string} */ + inputValue_: { + type: String, + value: '', + observer: 'onInputValueChange_', + }, + + /** @private {boolean} */ + passwordInvalid_: { + type: Boolean, + value: false, + }, + + /** @private {boolean} */ + waitingForPasswordCheck_: { + type: Boolean, + value: false, + }, + }, + + /** @private {?multidevice_setup.BrowserProxy} */ + browserProxy_: null, + + clearPasswordTextInput: function() { + this.$.passwordInput.value = ''; + }, + + /** @override */ + created: function() { + this.browserProxy_ = multidevice_setup.BrowserProxyImpl.getInstance(); + }, + + /** @override */ + attached: function() { + this.browserProxy_.getProfileInfo().then((profileInfo) => { + this.profilePhotoUrl_ = profileInfo.profilePhotoUrl; + this.email_ = profileInfo.email; + }); + }, + + /** Overridden from UiPageContainerBehavior. */ + getCanNavigateToNextPage: function() { + return new Promise((resolve) => { + if (this.waitingForPasswordCheck_) { + resolve(false /* canNavigate */); + return; + } + this.waitingForPasswordCheck_ = true; + this.quickUnlockPrivate_.getAuthToken(this.inputValue_, (tokenInfo) => { + this.waitingForPasswordCheck_ = false; + if (chrome.runtime.lastError) { + this.passwordInvalid_ = true; + // Select the password text if the user entered an incorrect password. + this.$.passwordInput.select(); + resolve(false /* canNavigate */); + return; + } + this.authToken = tokenInfo.token; + this.passwordInvalid_ = false; + resolve(true /* canNavigate */); + }); + }); + }, + + /** @private */ + onInputValueChange_: function() { + this.passwordInvalid_ = false; + }, + + /** + * @param {!Event} e + * @private + */ + onInputKeypress_: function(e) { + // We are only listening for the user trying to enter their password. + if (e.key != 'Enter') + return; + + this.fire('user-submitted-password'); + }, + + /** + * @return {boolean} Whether the forward button should be disabled. + * @private + */ + shouldForwardButtonBeDisabled_: function() { + return this.passwordInvalid_ || !this.inputValue_ || + this.waitingForPasswordCheck_; + }, +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.html new file mode 100644 index 00000000000..181687dffd7 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.html @@ -0,0 +1,18 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html"> +<link rel="import" href="chrome://resources/html/cr.html"> + +<dom-module id="setup-failed-page"> + <template> + <ui-page header-text="[[headerText]]" icon-name="error-icon"> + <span slot="message" inner-h-t-m-l="[[messageHtml]]"></span> + <div slot="additional-content"> + This is empty... (PlAcEhOlDeR tExT!!) + </div> + </ui-page> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/setup_failed_page.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.js new file mode 100644 index 00000000000..36f2d4ae117 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_failed_page.js @@ -0,0 +1,43 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +Polymer({ + is: 'setup-failed-page', + + properties: { + /** Overridden from UiPageContainerBehavior. */ + forwardButtonTextId: { + type: String, + value: 'tryAgain', + }, + + /** Overridden from UiPageContainerBehavior. */ + cancelButtonTextId: { + type: String, + value: 'cancel', + }, + + /** Overridden from UiPageContainerBehavior. */ + backwardButtonTextId: { + type: String, + value: 'back', + }, + + /** Overridden from UiPageContainerBehavior. */ + headerId: { + type: String, + value: 'setupFailedPageHeader', + }, + + /** Overridden from UiPageContainerBehavior. */ + messageId: { + type: String, + value: 'setupFailedPageMessage', + }, + }, + + behaviors: [ + UiPageContainerBehavior, + ], +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_1x.png b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_1x.png Binary files differnew file mode 100644 index 00000000000..03074bd5735 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_1x.png diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_2x.png b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_2x.png Binary files differnew file mode 100644 index 00000000000..271b0484e3e --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_icon_2x.png diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.html new file mode 100644 index 00000000000..a899610a5d0 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.html @@ -0,0 +1,35 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html"> +<link rel="import" href="chrome://resources/html/cr.html"> + +<dom-module id="setup-succeeded-page"> + <template> + <style include="multidevice-setup-shared"> + #page-icon-container { + @apply --layout-horizontal; + @apply --layout-center-justified; + } + + #page-icon { + background-image: -webkit-image-set( + url(setup_succeeded_icon_1x.png) 1x, + url(setup_succeeded_icon_2x.png) 2x); + height: 156px; + margin-top: 64px; + width: 416px; + } + </style> + <ui-page header-text="[[headerText]]" icon-name="google-g"> + <span slot="message" inner-h-t-m-l="[[messageHtml]]"></span> + <div id="page-icon-container" slot="additional-content"> + <div id="page-icon"></div> + </div> + </ui-page> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.js new file mode 100644 index 00000000000..e9c4eac1bc1 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/setup_succeeded_page.js @@ -0,0 +1,59 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.exportPath('multidevice_setup'); + +Polymer({ + is: 'setup-succeeded-page', + + properties: { + /** Overridden from UiPageContainerBehavior. */ + forwardButtonTextId: { + type: String, + value: 'done', + }, + + /** Overridden from UiPageContainerBehavior. */ + headerId: { + type: String, + value: 'setupSucceededPageHeader', + }, + + /** Overridden from UiPageContainerBehavior. */ + messageId: { + type: String, + value: 'setupSucceededPageMessage', + }, + }, + + behaviors: [ + UiPageContainerBehavior, + ], + + /** @private {?multidevice_setup.BrowserProxy} */ + browserProxy_: null, + + /** @override */ + created: function() { + this.browserProxy_ = multidevice_setup.BrowserProxyImpl.getInstance(); + }, + + /** @private */ + openSettings_: function() { + this.browserProxy_.openMultiDeviceSettings(); + }, + + /** @private */ + onSettingsLinkClicked_: function() { + this.openSettings_(); + this.fire('setup-exited'); + }, + + /** @override */ + ready: function() { + let linkElement = this.$$('#settings-link'); + linkElement.setAttribute('href', '#'); + linkElement.addEventListener('click', () => this.onSettingsLinkClicked_()); + }, +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_1x.png b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_1x.png Binary files differnew file mode 100644 index 00000000000..b6a3a196cb8 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_1x.png diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_2x.png b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_2x.png Binary files differnew file mode 100644 index 00000000000..c7234e8f191 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_icon_2x.png diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.html new file mode 100644 index 00000000000..a410816bbf7 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.html @@ -0,0 +1,135 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/icons.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html"> +<link rel="import" href="chrome://resources/html/cr.html"> +<link rel="import" href="chrome://resources/html/i18n_behavior.html"> +<link rel="import" href="chrome://resources/html/web_ui_listener_behavior.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html"> + +<dom-module id="start-setup-page"> + <template> + <style include="multidevice-setup-shared"> + #selector-and-details-container { + @apply --layout-horizontal; + margin-top: 48px; + min-height: 246px; + } + + #singleDeviceName { + color: var(--google-grey-900); + margin-top: 16px; + } + + #deviceDropdown { + margin-top: 16px; + } + + #page-icon-container { + @apply --layout-horizontal; + @apply --layout-center-justified; + } + + #page-icon { + background-image: -webkit-image-set(url(start_setup_icon_1x.png) 1x, + url(start_setup_icon_2x.png) 2x); + height: 116px; + margin-top: 10px; + width: 320px; + } + + #deviceSelectionContainer { + color: var(--paper-grey-600); + } + + #feature-details-container { + @apply --layout-vertical; + @apply --layout-center-justified; + border-left: 1px solid rgb(218, 220, 224); + padding-left: 24px; + } + + #feature-details-container-header { + margin-bottom: 18px; + } + + .feature-detail { + @apply --layout-horizontal; + @apply --layout-center; + box-sizing: border-box; + min-height: 64px; + padding: 10px 0; + } + + .feature-detail iron-icon { + --iron-icon-height: 20px; + --iron-icon-width: 20px; + min-width: 20px; + } + + .feature-detail span { + margin-left: 8px; + } + + #footnote { + color: var(--paper-grey-600); + margin-top: 12px; + } + </style> + + <ui-page header-text="[[headerText]]" icon-name="google-g"> + <span slot="message" id="multidevice-summary-message" inner-h-t-m-l="[[messageHtml]]"></span> + <div slot="additional-content"> + <div id="selector-and-details-container"> + <div id="deviceSelectionContainer" class="flex"> + [[getDeviceSelectionHeader_(devices)]] + <div class="flex"></div> + <div id="singleDeviceName" + hidden$="[[!doesDeviceListHaveOneElement_(devices)]]"> + [[getFirstDeviceNameInList_(devices)]] + </div> + <div hidden$="[[!doesDeviceListHaveMultipleElements_(devices)]]"> + <select id="deviceDropdown" + class="md-select" + on-change="onDeviceDropdownSelectionChanged_"> + <template is="dom-repeat" items="[[devices]]"> + <option value$="[[item.deviceId]]"> + [[item.deviceName]] + </option> + </template> + </select> + </div> + <div id="page-icon-container"> + <div id="page-icon"></div> + </div> + </div> + <div id="feature-details-container" class="flex"> + <div id="feature-details-container-header"> + [[i18n('startSetupPageFeatureListHeader')]] + </div> + <div class="feature-detail"> + <iron-icon icon="multidevice-setup-icons-20:messages"></iron-icon> + <span id="awm-summary-message" inner-h-t-m-l=" + [[i18nAdvanced('startSetupPageFeatureListAwm')]]"> + </span> + </div> + <div class="feature-detail"> + <iron-icon icon="multidevice-setup-icons-20:downloads"> + </iron-icon> + <span>[[i18n('startSetupPageFeatureListInstallApps')]]</span> + </div> + <div class="feature-detail"> + <iron-icon icon="multidevice-setup-icons-20:features"></iron-icon> + <span>[[i18n('startSetupPageFeatureListAddFeatures')]]</span> + </div> + </div> + </div> + <div id="footnote">[[i18n('startSetupPageFootnote')]]</div> + </div> + </ui-page> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/start_setup_page.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.js new file mode 100644 index 00000000000..d9f6db488e7 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/start_setup_page.js @@ -0,0 +1,155 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +Polymer({ + is: 'start-setup-page', + + properties: { + /** Overridden from UiPageContainerBehavior. */ + forwardButtonTextId: { + type: String, + value: 'accept', + }, + + /** Overridden from UiPageContainerBehavior. */ + cancelButtonTextId: { + type: String, + computed: 'getCancelButtonTextId_(delegate)', + }, + + /** Overridden from UiPageContainerBehavior. */ + headerId: { + type: String, + value: 'startSetupPageHeader', + }, + + /** Overridden from UiPageContainerBehavior. */ + messageId: { + type: String, + value: 'startSetupPageMessage', + }, + + /** + * Array of objects representing all potential MultiDevice hosts. + * + * @type {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} + */ + devices: { + type: Array, + value: () => [], + observer: 'devicesChanged_', + }, + + /** + * Unique identifier for the currently selected host device. + * + * Undefined if the no list of potential hosts has been received from mojo + * service. + * + * @type {string|undefined} + */ + selectedDeviceId: { + type: String, + notify: true, + }, + + /** + * Delegate object which performs differently in OOBE vs. non-OOBE mode. + * @type {!multidevice_setup.MultiDeviceSetupDelegate} + */ + delegate: Object, + }, + + behaviors: [ + UiPageContainerBehavior, + I18nBehavior, + WebUIListenerBehavior, + ], + + /** @override */ + attached: function() { + this.addWebUIListener( + 'multidevice_setup.initializeSetupFlow', + this.initializeSetupFlow_.bind(this)); + }, + + /** @private */ + initializeSetupFlow_: function() { + // The "Learn More" links are inside a grdp string, so we cannot actually + // add an onclick handler directly to the html. Instead, grab the two and + // manaully add onclick handlers. + let helpArticleLinks = [ + this.$$('#multidevice-summary-message a'), + this.$$('#awm-summary-message a') + ]; + for (let i = 0; i < helpArticleLinks.length; i++) { + helpArticleLinks[i].onclick = this.fire.bind( + this, 'open-learn-more-webview-requested', helpArticleLinks[i].href); + } + }, + + /** + * @param {!multidevice_setup.MultiDeviceSetupDelegate} delegate + * @return {string} The cancel button text ID, dependent on OOBE vs. non-OOBE. + * @private + */ + getCancelButtonTextId_: function(delegate) { + return this.delegate.getStartSetupCancelButtonTextId(); + }, + + /** + * @param {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} devices + * @return {string} Label for devices selection content. + * @private + */ + getDeviceSelectionHeader_(devices) { + switch (devices.length) { + case 0: + return ''; + case 1: + return this.i18n('startSetupPageSingleDeviceHeader'); + default: + return this.i18n('startSetupPageMultipleDeviceHeader'); + } + }, + + /** + * @param {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} devices + * @return {boolean} True if there are more than one potential host devices. + * @private + */ + doesDeviceListHaveMultipleElements_: function(devices) { + return devices.length > 1; + }, + + /** + * @param {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} devices + * @return {boolean} True if there is exactly one potential host device. + * @private + */ + doesDeviceListHaveOneElement_: function(devices) { + return devices.length == 1; + }, + + /** + * @param {!Array<!chromeos.deviceSync.mojom.RemoteDevice>} devices + * @return {string} Name of the first device in device list if there are any. + * Returns an empty string otherwise. + * @private + */ + getFirstDeviceNameInList_: function(devices) { + return devices[0] ? this.devices[0].deviceName : ''; + }, + + /** @private */ + devicesChanged_: function() { + if (this.devices.length > 0) + this.selectedDeviceId = this.devices[0].deviceId; + }, + + /** @private */ + onDeviceDropdownSelectionChanged_: function() { + this.selectedDeviceId = this.$.deviceDropdown.value; + }, +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.html new file mode 100644 index 00000000000..289e08644b8 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.html @@ -0,0 +1,42 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/icons.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html"> + +<dom-module id="ui-page"> + <template> + <style include="multidevice-setup-shared"> + iron-icon { + --iron-icon-width: 32px; + --iron-icon-height: 32px; + } + + h1 { + color: var(--google-grey-900); + font-family: 'Google Sans'; + font-size: 28px; + font-weight: normal; + line-height: 28px; + margin: 0; + padding-top: 36px; + } + + #message-container { + box-sizing: border-box; + min-height: 32px; + padding-top: 16px; + } + </style> + <iron-icon icon="[[computeIconIdentifier_(iconName)]]"></iron-icon> + <h1>[[headerText]]</h1> + <div id="message-container"> + <slot name="message"></slot> + </div> + <div id="additional-content-container"> + <slot name="additional-content"></slot> + </div> + </template> + <script src="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.js new file mode 100644 index 00000000000..d29f7722d10 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page.js @@ -0,0 +1,34 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * An element that encapsulates the structure common to all pages in the WebUI. + */ +Polymer({ + is: 'ui-page', + + properties: { + /** + * Main heading for the page. + * + * @type {string} + */ + headerText: String, + + /** + * Name of icon within icon set. + * + * @type {string} + */ + iconName: String, + }, + + /** + * @return {string} + * @private + */ + computeIconIdentifier_: function() { + return 'multidevice-setup-icons-32:' + this.iconName; + }, +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html new file mode 100644 index 00000000000..501cc374471 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html @@ -0,0 +1,5 @@ +<link rel="import" href="chrome://resources/html/cr.html"> +<link rel="import" href="chrome://resources/html/i18n_behavior.html"> + +<script src="chrome://resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.js"> +</script> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.js b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.js new file mode 100644 index 00000000000..e9f16b9ee89 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/multidevice_setup/ui_page_container_behavior.js @@ -0,0 +1,141 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** @polymerBehavior */ +const UiPageContainerBehaviorImpl = { + properties: { + /** + * ID for forward button label, which must be translated for display. + * + * Undefined if the visible page has no forward-navigation button. + * + * @type {string|undefined} + */ + forwardButtonTextId: String, + + /** + * ID for cancel button label, which must be translated for display. + * + * Undefined if the visible page has no cancel button. + * + * @type {string|undefined} + */ + cancelButtonTextId: String, + + /** + * ID for backward button label, which must be translated for display. + * + * Undefined if the visible page has no backward-navigation button. + * + * @type {string|undefined} + */ + backwardButtonTextId: String, + + /** + * ID for text of main UI Page heading. + * + * @type {string} + */ + headerId: String, + + /** + * ID for text of main UI Page message body. + * + * @type {string} + */ + messageId: String, + + /** + * Translated text to display on the forward-naviation button. + * + * Undefined if the visible page has no forward-navigation button. + * + * @type {string|undefined} + */ + forwardButtonText: { + type: String, + computed: 'computeLocalizedText_(forwardButtonTextId)', + }, + + /** + * Translated text to display on the cancel button. + * + * Undefined if the visible page has no cancel button. + * + * @type {string|undefined} + */ + cancelButtonText: { + type: String, + computed: 'computeLocalizedText_(cancelButtonTextId)', + }, + + /** + * Translated text to display on the backward-naviation button. + * + * Undefined if the visible page has no backward-navigation button. + * + * @type {string|undefined} + */ + backwardButtonText: { + type: String, + computed: 'computeLocalizedText_(backwardButtonTextId)', + }, + + /** + * Translated text of main UI Page heading. + * + * @type {string|undefined} + */ + headerText: { + type: String, + computed: 'computeLocalizedText_(headerId)', + }, + + /** + * Translated text of main UI Page heading. In general this can include + * some markup. + * + * @type {string|undefined} + */ + messageHtml: { + type: String, + computed: 'computeLocalizedText_(messageId)', + }, + }, + + /** + * Returns a promise which always resolves and returns a boolean representing + * whether it should be possible to navigate forward. This function is called + * before forward navigation is requested; if false is returned, the active + * page does not change. + * @return {!Promise} + */ + getCanNavigateToNextPage: function() { + return new Promise((resolve) => { + resolve(true /* canNavigate */); + }); + }, + + /** + * @param {string} textId Key for the localized string to appear on a + * button. + * @return {string|undefined} The localized string corresponding to the key + * textId. Return value is undefined if textId is not a key + * for any localized string. Note: this includes the case in which + * textId is undefined. + * @private + */ + computeLocalizedText_: function(textId) { + if (!this.i18nExists(textId)) + return; + + return loadTimeData.getString(textId); + }, +}; + +/** @polymerBehavior */ +const UiPageContainerBehavior = [ + I18nBehavior, + UiPageContainerBehaviorImpl, +]; diff --git a/chromium/ui/webui/resources/cr_components/chromeos/network/network_config.html b/chromium/ui/webui/resources/cr_components/chromeos/network/network_config.html index 4c506f20691..f6053ce9367 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/network/network_config.html +++ b/chromium/ui/webui/resources/cr_components/chromeos/network/network_config.html @@ -154,7 +154,7 @@ <cr-toggle id="share" checked="{{shareNetwork_}}" disabled="[[!shareIsEnabled_(guid, configProperties_.*, security_, eapProperties_.*, shareAllowEnable)]]" - aria-label="[[i18n('networkConfigShare')]]"> + aria-labeledby="shareLabel"> </cr-toggle> </div> </template> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/network/network_proxy.js b/chromium/ui/webui/resources/cr_components/chromeos/network/network_proxy.js index b5dc5c2bbc2..c827a0ddfe5 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/network/network_proxy.js +++ b/chromium/ui/webui/resources/cr_components/chromeos/network/network_proxy.js @@ -191,7 +191,8 @@ Polymer({ (CrOnc.proxyMatches(jsonHttp, proxy.Manual.SecureHTTPProxy) && CrOnc.proxyMatches(jsonHttp, proxy.Manual.FTPProxy) && CrOnc.proxyMatches(jsonHttp, proxy.Manual.SOCKS)) || - (!proxy.Manual.SecureHTTPProxy.Host && + (!proxy.Manual.HTTPProxy.Host && + !proxy.Manual.SecureHTTPProxy.Host && !proxy.Manual.FTPProxy.Host && !proxy.Manual.SOCKS.Host); } if (proxySettings.ExcludeDomains) { diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/BUILD.gn b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/BUILD.gn index a024915b51f..afe7eb05b32 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/BUILD.gn +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/BUILD.gn @@ -7,6 +7,7 @@ import("//third_party/closure_compiler/compile_js.gni") js_type_check("closure_compile") { deps = [ ":pin_keyboard", + ":setup_pin_keyboard", ] } @@ -17,3 +18,21 @@ js_library("pin_keyboard") { "//ui/webui/resources/js:i18n_behavior", ] } + +js_library("lock_screen_constants") { + deps = [ + "//ui/webui/resources/cr_elements/cr_profile_avatar_selector:cr_profile_avatar_selector", + "//ui/webui/resources/js:cr", + ] +} + +js_library("setup_pin_keyboard") { + deps = [ + ":lock_screen_constants", + ":pin_keyboard", + "//ui/webui/resources/cr_components/chromeos/quick_unlock:lock_screen_constants", + "//ui/webui/resources/js:i18n_behavior", + ] + externs_list = [ "$externs_path/quick_unlock_private.js" ] + extra_sources = [ "$interfaces_path/quick_unlock_private_interface.js" ] +} diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.html b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.html new file mode 100644 index 00000000000..29c7f5cb64f --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.html @@ -0,0 +1 @@ +<script src="chrome://resources/cr_components/chromeos/quick_unlock/lock_screen_constants.js"></script> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.js b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.js new file mode 100644 index 00000000000..e5ee46e6d1a --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/lock_screen_constants.js @@ -0,0 +1,47 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Constants used for logging the pin unlock setup uma. + */ + +/** + * Name of the pin unlock setup uma histogram. + * @type {string} + */ +const PinUnlockUmaHistogramName = 'Settings.PinUnlockSetup'; + +/** + * Stages the user can enter while setting up pin unlock. + * @enum {number} + */ +const LockScreenProgress = { + START_SCREEN_LOCK: 0, + ENTER_PASSWORD_CORRECTLY: 1, + CHOOSE_PIN_OR_PASSWORD: 2, + ENTER_PIN: 3, + CONFIRM_PIN: 4, + MAX_BUCKET: 5 +}; + +cr.define('settings', function() { + /** + * Helper function to send the progress of the pin setup to be recorded in the + * histogram. + * @param {LockScreenProgress} currentProgress + */ + const recordLockScreenProgress = function(currentProgress) { + if (currentProgress >= LockScreenProgress.MAX_BUCKET) { + console.error( + 'Expected a enumeration value of ' + LockScreenProgress.MAX_BUCKET + + ' or lower: Received ' + currentProgress + '.'); + return; + } + chrome.send('metricsHandler:recordInHistogram', [ + PinUnlockUmaHistogramName, currentProgress, LockScreenProgress.MAX_BUCKET + ]); + }; + + return {recordLockScreenProgress: recordLockScreenProgress}; +}); diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.html b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.html index f28f89b9e06..080f3cc0328 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.html +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.html @@ -36,16 +36,41 @@ <template> <style include="cr-shared-style"> :host { + --backspace-button-ripple-left: calc((var(--backspace-button-width) - + var(--pin-button-ripple-width)) / 2); + --backspace-button-width: calc(var(--pin-button-width) + + var(--pin-button-horizontal-margin) * 2); + --pin-button-height: 40px; + --pin-button-horizontal-margin: 20px; + --pin-button-ripple-height: 48px; + --pin-button-ripple-left: calc((var(--pin-button-width) - + var(--pin-button-ripple-width)) / 2); + --pin-button-ripple-top: calc((var(--pin-button-height) - + var(--pin-button-ripple-height)) / 2); + --pin-button-ripple-width: 48px; + --pin-button-vertical-margin: 8px; + --pin-button-width: 40px; outline: none; } #root { + align-items: center; + display: flex; + flex-direction: column; + min-height: 0; + } + + #rowsContainer { direction: ltr; display: block; + width: calc((var(--pin-button-width) + + var(--pin-button-horizontal-margin) * 2) * 3); } .row { display: flex; + margin-bottom: calc(var(--pin-button-vertical-margin) * 2); + min-height: 0; } :host([enable-password]) #pinInputDiv { @@ -57,10 +82,10 @@ } #backspaceButton { - color: var(--pin-keyboard-backspace-color, #000); + color: var(--pin-keyboard-backspace-color, var(--google-grey-700)); left: 0; opacity: var(--pin-keyboard-backspace-opacity, --dark-primary-opacity); - padding: 14px; + padding: 12px; position: absolute; top: 0; } @@ -71,75 +96,93 @@ #backspaceButtonContainer { position: relative; + width: var(--backspace-button-width); } #backspaceButtonContainer paper-ripple { - left: var(--pin-keyboard-backspace-paper-ripple-offset, 0); - top: var(--pin-keyboard-backspace-paper-ripple-offset, 0); + left: var(--pin-keyboard-backspace-paper-ripple-offset, + var(--backspace-button-ripple-left)); + top: var(--pin-keyboard-backspace-paper-ripple-offset, + var(--pin-button-ripple-top)); } .digit-button { + --paper-button: { + min-width: 0; + }; align-items: center; background: none; border-radius: 0; box-sizing: border-box; - color: #000; + color: var(--google-grey-900); display: flex; flex-direction: column; - height: 48px; + height: var(--pin-button-height) justify-content: center; - margin: 0; - min-height: 48px; - min-width: 48px; + margin: 0 var(--pin-button-horizontal-margin); + min-height: 0; opacity: 0.87px; - width: 60px; + padding: 0; + width: var(--pin-button-width); @apply --pin-keyboard-digit-button; } .digit-button inner-text { - display: flex; - flex-direction: column; font-family: 'Roboto'; } - .letter { - color: var(--pin-keyboard-letter-color, --paper-blue-grey-700); - font-size: 9px; - margin-top: 4px; + inner-text.letter { + color: var(--pin-keyboard-letter-color, var(--google-grey-700)); + font-size: 12px; + margin-top: 8px; + + @apply --pin-keyboard-digit-button-letter; } .number { - color: var(--pin-keyboard-number-color, --paper-blue-grey-700); - font-size: 20px; - height: 52px; + color: var(--pin-keyboard-number-color, var(--paper-blue-grey-700)); + font-size: 18px; + height: 16px; } paper-ripple { color: var(--google-grey-700); - height: 48px; - left: 6px; - width: 48px; + height: var(--pin-button-ripple-height); + left: var(--pin-button-ripple-left); + top: var(--pin-button-ripple-top); + width: var(--pin-button-ripple-width); @apply --pin-keyboard-paper-ripple; } #pinInput { + --cr-input-error-display: none; + --cr-input-input: { + font-size: 28px; + letter-spacing: 28px; + }; + --cr-input-padding-bottom: 1px; + --cr-input-padding-end: 0; + --cr-input-padding-start: 0; + --cr-input-padding-top: 1px; background-color: white; border: 0; box-sizing: border-box; font-face: Roboto-Regular; font-size: 13px; - height: 43px; left: 0; opacity: var(--dark-secondary-opacity); outline: 0; position: relative; text-align: center; - width: 180px; + width: 200px; + + @apply --pin-keyboard-pin-input-style; } #pinInput[has-content] { + --cr-disabled-opacity: var(--dark-primary-opacity); opacity: var(--dark-primary-opacity); } @@ -155,93 +198,98 @@ </style> <div id="root" on-contextmenu="onContextMenu_" on-tap="focusInput_"> - <div id="pinInputDiv" class="row"> + <div id="pinInputDiv"> <cr-input id="pinInput" type="password" value="{{value}}" is-input-rtl$="[[isInputRtl_(value)]]" has-content$="[[hasInput_(value)]]" invalid="[[hasError]]" - placeholder="[[getInputPlaceholder_(enablePassword)]]" - on-keydown="onInputKeyDown_"> + placeholder="[[getInputPlaceholder_(enablePassword, + enablePlaceholder)]]" + on-keydown="onInputKeyDown_" force-underline$="[[forceUnderline_]]" + disabled="[[isIncognitoUi]]"> </cr-input> </div> <slot select="[problem]"></slot> - <div class="row"> - <paper-button class="digit-button" on-tap="onNumberTap_" value="1" - noink> - <inner-text class="number">[[i18n('pinKeyboard1')]]</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="2" - noink> - <inner-text class="number">[[i18n('pinKeyboard2')]]</inner-text> - <inner-text class="letter">ABC</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="3" - noink> - <inner-text class="number">[[i18n('pinKeyboard3')]]</inner-text> - <inner-text class="letter">DEF</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - </div> - <div class="row"> - <paper-button class="digit-button" on-tap="onNumberTap_" value="4" - noink> - <inner-text class="number">[[i18n('pinKeyboard4')]]</inner-text> - <inner-text class="letter">GHI</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="5" - noink> - <inner-text class="number">[[i18n('pinKeyboard5')]]</inner-text> - <inner-text class="letter">JKL</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="6" - noink> - <inner-text class="number">[[i18n('pinKeyboard6')]]</inner-text> - <inner-text class="letter">MNO</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - </div> - <div class="row"> - <paper-button class="digit-button" on-tap="onNumberTap_" value="7" - noink> - <inner-text class="number">[[i18n('pinKeyboard7')]]</inner-text> - <inner-text class="letter">PQRS</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="8" - noink> - <inner-text class="number">[[i18n('pinKeyboard8')]]</inner-text> - <inner-text class="letter">TUV</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <paper-button class="digit-button" on-tap="onNumberTap_" value="9" - noink> - <inner-text class="number">[[i18n('pinKeyboard9')]]</inner-text> - <inner-text class="letter">WXYZ</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - </div> - <div class="row bottom-row"> - <div class="digit-button"></div> - <paper-button class="digit-button" on-tap="onNumberTap_" value="0" - noink> - <inner-text class="number">[[i18n('pinKeyboard0')]]</inner-text> - <inner-text class="letter">+</inner-text> - <paper-ripple class="circle" center></paper-ripple> - </paper-button> - <div id="backspaceButtonContainer"> - <paper-icon-button id="backspaceButton" class="digit-button" - disabled$="[[!hasInput_(value)]]" - icon="pin-keyboard:backspace" - on-pointerdown="onBackspacePointerDown_" - on-pointerout="clearAndReset_" - on-pointerup="onBackspacePointerUp_" - title="[[i18n('pinKeyboardDeleteAccessibleName')]]" + <div id="rowsContainer"> + <div class="row"> + <paper-button class="digit-button" on-tap="onNumberTap_" value="1" + noink> + <inner-text class="number">[[i18n('pinKeyboard1')]]</inner-text> + <inner-text class="letter"> </inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="2" + noink> + <inner-text class="number">[[i18n('pinKeyboard2')]]</inner-text> + <inner-text class="letter">ABC</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="3" + noink> + <inner-text class="number">[[i18n('pinKeyboard3')]]</inner-text> + <inner-text class="letter">DEF</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + </div> + <div class="row"> + <paper-button class="digit-button" on-tap="onNumberTap_" value="4" + noink> + <inner-text class="number">[[i18n('pinKeyboard4')]]</inner-text> + <inner-text class="letter">GHI</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="5" + noink> + <inner-text class="number">[[i18n('pinKeyboard5')]]</inner-text> + <inner-text class="letter">JKL</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="6" + noink> + <inner-text class="number">[[i18n('pinKeyboard6')]]</inner-text> + <inner-text class="letter">MNO</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + </div> + <div class="row"> + <paper-button class="digit-button" on-tap="onNumberTap_" value="7" + noink> + <inner-text class="number">[[i18n('pinKeyboard7')]]</inner-text> + <inner-text class="letter">PQRS</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="8" + noink> + <inner-text class="number">[[i18n('pinKeyboard8')]]</inner-text> + <inner-text class="letter">TUV</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <paper-button class="digit-button" on-tap="onNumberTap_" value="9" + noink> + <inner-text class="number">[[i18n('pinKeyboard9')]]</inner-text> + <inner-text class="letter">WXYZ</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + </div> + <div class="row bottom-row"> + <div class="digit-button"></div> + <paper-button class="digit-button" on-tap="onNumberTap_" value="0" noink> - </paper-icon-button> - <paper-ripple class="circle" center></paper-ripple> + <inner-text class="number">[[i18n('pinKeyboard0')]]</inner-text> + <inner-text class="letter">+</inner-text> + <paper-ripple class="circle" center></paper-ripple> + </paper-button> + <div id="backspaceButtonContainer"> + <paper-icon-button id="backspaceButton" class="digit-button" + disabled$="[[!hasInput_(value)]]" + icon="pin-keyboard:backspace" + on-pointerdown="onBackspacePointerDown_" + on-pointerout="clearAndReset_" + on-pointerup="onBackspacePointerUp_" + title="[[i18n('pinKeyboardDeleteAccessibleName')]]" + noink> + </paper-icon-button> + <paper-ripple class="circle" center></paper-ripple> + </div> </div> </div> </div> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.js b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.js index 0f7e3da6463..ceaaae8adca 100644 --- a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.js +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/pin_keyboard.js @@ -29,7 +29,7 @@ * @type {number} * @const */ -var REPEAT_BACKSPACE_DELAY_MS = 150; +const REPEAT_BACKSPACE_DELAY_MS = 150; /** * How long the backspace button must be held down before auto backspace @@ -37,7 +37,7 @@ var REPEAT_BACKSPACE_DELAY_MS = 150; * @type {number} * @const */ -var INITIAL_BACKSPACE_DELAY_MS = 500; +const INITIAL_BACKSPACE_DELAY_MS = 500; /** * The key codes of the keys allowed to be used on the pin input, in addition to @@ -45,7 +45,7 @@ var INITIAL_BACKSPACE_DELAY_MS = 500; * @type {Array<number>} * @const */ -var PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = [8, 9, 37, 39]; +const PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = [8, 9, 37, 39]; Polymer({ is: 'pin-keyboard', @@ -103,6 +103,36 @@ Polymer({ value: '', observer: 'onPinValueChange_', }, + + /** + * @private + */ + forceUnderline_: { + type: Boolean, + value: false, + }, + + /** + * Enables pin placeholder. + */ + enablePlaceholder: { + type: Boolean, + value: false, + }, + + /** + * Turns on "incognito mode". (FIXME after https://crbug.com/900351 is + * fixed). + */ + isIncognitoUi: { + type: Boolean, + value: false, + }, + }, + + listeners: { + 'blur': 'onBlur_', + 'focus': 'onFocus_', }, /** @@ -174,17 +204,27 @@ Polymer({ this.focus(this.selectionStart_, this.selectionEnd_); }, + /** @private */ + onFocus_: function() { + this.forceUnderline_ = true; + }, + + /** @private */ + onBlur_: function() { + this.forceUnderline_ = false; + }, + /** * Called when a keypad number has been tapped. * @param {Event} event The event object. * @private */ onNumberTap_: function(event) { - var numberValue = event.target.getAttribute('value'); + let numberValue = event.target.getAttribute('value'); // Add the number where the caret is, then update the selection range of the // input element. - var selectionStart = this.selectionStart_; + let selectionStart = this.selectionStart_; this.value = this.value.substring(0, this.selectionStart_) + numberValue + this.value.substring(this.selectionEnd_); @@ -223,8 +263,8 @@ Polymer({ // If the input is shown, clear the text based on the caret location or // selected region of the input element. If it is just a caret, remove the // character in front of the caret. - var selectionStart = this.selectionStart_; - var selectionEnd = this.selectionEnd_; + let selectionStart = this.selectionStart_; + let selectionEnd = this.selectionEnd_; if (selectionStart == selectionEnd && selectionStart) selectionStart--; @@ -355,9 +395,13 @@ Polymer({ /** * Computes the value of the pin input placeholder. * @param {boolean} enablePassword + * @param {boolean} enablePlaceholder * @private */ - getInputPlaceholder_: function(enablePassword) { + getInputPlaceholder_: function(enablePassword, enablePlaceholder) { + if (!enablePlaceholder) + return ''; + return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') : this.i18n('pinKeyboardPlaceholderPin'); }, diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.html b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.html new file mode 100644 index 00000000000..71e9a15fd8e --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.html @@ -0,0 +1,103 @@ +<link rel="import" href="chrome://resources/html/polymer.html"> + +<link rel="import" href="chrome://resources/cr_components/chromeos/quick_unlock/lock_screen_constants.html"> +<link rel="import" href="chrome://resources/cr_components/chromeos/quick_unlock/pin_keyboard.html"> +<link rel="import" href="chrome://resources/cr_elements/cr_dialog/cr_dialog.html"> +<link rel="import" href="chrome://resources/cr_elements/icons.html"> +<link rel="import" href="chrome://resources/cr_elements/shared_vars_css.html"> +<link rel="import" href="chrome://resources/html/assert.html"> +<link rel="import" href="chrome://resources/html/i18n_behavior.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html"> + +<!-- + +This module is a "pin setup" keyboard + pin display element. +It can be integrated into some UI container to set pin unlock. + +Usage: + <setup-pin-keyboard + enable-submit="{{enableSubmit_}}" + is-confirm-step="{{isConfirmStep_}}" + on-pin-submit="onPinSubmit_" + on-set-pin-done="onSetPinDone_" + set-modes="{{setModes}}"> + </setup-pin-keyboard> + +Where: + * enable-submit - Notification property for the container to enable/disable + submit button in the container (if it exists). True when pin can be + submitted. + * is-confirm-step - Notification property for the container to update UI + when pin confirmation is requested. False when initial PIN entry step + is active, true when pin confirmation is active. + * on-pin-submit - Event handler for the user requested pin submit by pressing + "Enter" key on the keyboard. setup-pin-keyboard will + not submit pin automatically, delegating this step to outer container. + Container must call setup-pin-keyboard.doSubmit() when + pin should be submitted. + * on-set-pin-done - Event handler for the "set pin done" event, which should + normally close the pin setup UI. This object state is reset before + sending this event. + * set-modes - Reflects property set in password_prompt_dialog.js. + +--> + +<dom-module id="setup-pin-keyboard"> + <template> + <style include="settings-shared"> + .error { + color: var(--google-red-600); + } + + .error > iron-icon { + --iron-icon-fill-color: var(--google-red-600); + } + + .warning { + color: var(--cr-secondary-text-color); + } + + .warning > iron-icon { + --iron-icon-fill-color: var(--google-grey-refresh-700); + } + + #problemDiv { + align-items: center; + display: flex; + flex-direction: row; + height: 32px; + min-height: 0; + } + + /* Hide this using visibility: hidden instead of hidden so that the + dialog does not resize when there are no problems to display. */ + #problemDiv[invisible] { + visibility: hidden; + } + + #problemMessage { + font-size: 10px; + } + </style> + <pin-keyboard id="pinKeyboard" on-pin-change="onPinChange_" + on-submit="onPinSubmit_" value="{{pinKeyboardValue_}}" + has-error="[[hasError_(problemMessageId_, problemClass_)]]" + enable-placeholder="[[enablePlaceholder]]" + is-incognito-ui="[[isIncognitoUi]]"> + <!-- Warning/error; only shown if title is hidden. --> + <div id="problemDiv" class$="[[problemClass_]]" + invisible$="[[!problemMessageId_]]" problem> + <div> + <iron-icon icon="cr:error-outline"></iron-icon> + <span id="problemMessage"> + [[formatProblemMessage_(locale, problemMessageId_, + problemMessageParameters_)]] + </span> + </div> + </div> + </pin-keyboard> + </template> + <script + src="chrome://resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.js"> + </script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.js b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.js new file mode 100644 index 00000000000..9dde5f4717f --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/chromeos/quick_unlock/setup_pin_keyboard.js @@ -0,0 +1,377 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * 'setup-pin-keyboard' is the keyboard/input field for choosing a PIN. + * + * See usage documentation in setup_pin_keyboard.html. + * + */ + +(function() { +'use strict'; + +/** + * Keep in sync with the string keys provided by settings. + * @enum {string} + */ +const MessageType = { + TOO_SHORT: 'configurePinTooShort', + TOO_LONG: 'configurePinTooLong', + TOO_WEAK: 'configurePinWeakPin', + MISMATCH: 'configurePinMismatched' +}; + +/** @enum {string} */ +const ProblemType = { + WARNING: 'warning', + ERROR: 'error' +}; + +Polymer({ + is: 'setup-pin-keyboard', + + behaviors: [I18nBehavior], + + properties: { + /** + * Reflects property set in password_prompt_dialog.js. + * @type {?Object} + */ + setModes: { + type: Object, + notify: true, + }, + + /** + * The current PIN keyboard value. + * @private + */ + pinKeyboardValue_: String, + + /** + * Stores the initial PIN value so it can be confirmed. + * @private + */ + initialPin_: String, + + /** + * The message ID of actual problem message to display. + * @private + */ + problemMessageId_: { + type: String, + value: '', + }, + + /** + * The additional parameters to format for the problem message string. + * @private + */ + problemMessageParameters_: { + type: String, + value: '', + }, + + /** + * The type of problem class to show (warning or error). + * @private + */ + problemClass_: String, + + /** + * Should the step-specific submit button be displayed? + * This has upward data flow only. + */ + enableSubmit: { + notify: true, + type: Boolean, + value: false, + }, + + /** + * writeUma is a function that handles writing uma stats. + * + * @type {function(LockScreenProgress)} + */ + writeUma: { + type: Object, + value: function() { + return function() {}; + } + }, + + /** + * The current step/subpage we are on. + * This is has upward data flow only. + */ + isConfirmStep: { + notify: true, + type: Boolean, + value: false, + }, + + /** + * Interface for chrome.quickUnlockPrivate calls. + * @type {QuickUnlockPrivate} + */ + quickUnlockPrivate: Object, + + /** + * |pinHasPassedMinimumLength_| tracks whether a user has passed the minimum + * length threshold at least once, and all subsequent PIN too short messages + * will be displayed as errors. They will be displayed as warnings prior to + * this. + * @private + */ + pinHasPassedMinimumLength_: {type: Boolean, value: false}, + + /** + * Enables pin placeholder. + */ + enablePlaceholder: { + type: Boolean, + value: false, + }, + + /** + * Turns on "incognito mode". (FIXME after https://crbug.com/900351 is + * fixed). + */ + isIncognitoUi: { + type: Boolean, + value: false, + }, + }, + + focus: function() { + this.$.pinKeyboard.focus(); + }, + + /** @override */ + attached: function() { + this.resetState(); + + // Show the pin is too short error when first displaying the PIN dialog. + this.problemClass_ = ProblemType.WARNING; + this.quickUnlockPrivate.getCredentialRequirements( + chrome.quickUnlockPrivate.QuickUnlockMode.PIN, + this.processPinRequirements_.bind(this, MessageType.TOO_SHORT)); + }, + + /** + * Resets the element to the initial state. + */ + resetState: function() { + this.initialPin_ = ''; + this.pinKeyboardValue_ = ''; + this.enableSubmit = false; + this.isConfirmStep = false; + this.hideProblem_(); + this.onPinChange_(); + }, + + /** + * Returns true if the PIN is ready to be changed to a new value. + * @private + * @return {boolean} + */ + canSubmit_: function() { + return this.initialPin_ == this.pinKeyboardValue_; + }, + + /** + * Handles writing the appropriate message to |problemMessageId_| && + * |problemMessageParameters_|. + * @private + * @param {string} messageId + * @param {chrome.quickUnlockPrivate.CredentialRequirements} requirements + * The requirements received from getCredentialRequirements. + */ + processPinRequirements_: function(messageId, requirements) { + let additionalInformation = ''; + switch (messageId) { + case MessageType.TOO_SHORT: + additionalInformation = requirements.minLength.toString(); + break; + case MessageType.TOO_LONG: + additionalInformation = (requirements.maxLength + 1).toString(); + break; + case MessageType.TOO_WEAK: + case MessageType.MISMATCH: + break; + default: + assertNotReached(); + break; + } + this.problemMessageId_ = messageId; + this.problemMessageParameters_ = additionalInformation; + }, + + /** + * Notify the user about a problem. + * @private + * @param {string} messageId + * @param {string} problemClass + */ + showProblem_: function(messageId, problemClass) { + this.quickUnlockPrivate.getCredentialRequirements( + chrome.quickUnlockPrivate.QuickUnlockMode.PIN, + this.processPinRequirements_.bind(this, messageId)); + this.problemClass_ = problemClass; + this.updateStyles(); + this.enableSubmit = + problemClass != ProblemType.ERROR && messageId != MessageType.TOO_SHORT; + }, + + /** @private */ + hideProblem_: function() { + this.problemMessageId_ = ''; + this.problemClass_ = ''; + }, + + /** + * Processes the message received from the quick unlock api and hides/shows + * the problem based on the message. + * @private + * @param {chrome.quickUnlockPrivate.CredentialCheck} message The message + * received from checkCredential. + */ + processPinProblems_: function(message) { + if (!message.errors.length && !message.warnings.length) { + this.hideProblem_(); + this.enableSubmit = true; + this.pinHasPassedMinimumLength_ = true; + return; + } + + if (!message.errors.length || + message.errors[0] != + chrome.quickUnlockPrivate.CredentialProblem.TOO_SHORT) { + this.pinHasPassedMinimumLength_ = true; + } + + if (message.warnings.length) { + assert( + message.warnings[0] == + chrome.quickUnlockPrivate.CredentialProblem.TOO_WEAK); + this.showProblem_(MessageType.TOO_WEAK, ProblemType.WARNING); + } + + if (message.errors.length) { + switch (message.errors[0]) { + case chrome.quickUnlockPrivate.CredentialProblem.TOO_SHORT: + this.showProblem_( + MessageType.TOO_SHORT, + this.pinHasPassedMinimumLength_ ? ProblemType.ERROR : + ProblemType.WARNING); + break; + case chrome.quickUnlockPrivate.CredentialProblem.TOO_LONG: + this.showProblem_(MessageType.TOO_LONG, ProblemType.ERROR); + break; + case chrome.quickUnlockPrivate.CredentialProblem.TOO_WEAK: + this.showProblem_(MessageType.TOO_WEAK, ProblemType.ERROR); + break; + default: + assertNotReached(); + break; + } + } + }, + + /** @private */ + onPinChange_: function() { + if (!this.isConfirmStep) { + if (this.pinKeyboardValue_) { + this.quickUnlockPrivate.checkCredential( + chrome.quickUnlockPrivate.QuickUnlockMode.PIN, + this.pinKeyboardValue_, this.processPinProblems_.bind(this)); + } else { + this.enableSubmit = false; + } + return; + } + + this.hideProblem_(); + this.enableSubmit = this.pinKeyboardValue_.length > 0; + }, + + /** @private */ + onPinSubmit_: function() { + // Notify container object. + this.fire('pin-submit'); + }, + + /** + * This is callback for quickUnlockPrivate.QuickUnlockMode.PIN API. + * + * @private + * @param {boolean} didSet + */ + onSetModesCompleted_: function(didSet) { + if (!didSet) { + console.error('Failed to update pin'); + return; + } + + this.resetState(); + this.fire('set-pin-done'); + }, + + /** This is called by container object when user initiated submit. */ + doSubmit: function() { + if (!this.isConfirmStep) { + if (!this.enableSubmit) + return; + this.initialPin_ = this.pinKeyboardValue_; + this.pinKeyboardValue_ = ''; + this.isConfirmStep = true; + this.onPinChange_(); + this.$.pinKeyboard.focus(); + this.writeUma(LockScreenProgress.ENTER_PIN); + return; + } + // onPinSubmit gets called if the user hits enter on the PIN keyboard. + // The PIN is not guaranteed to be valid in that case. + if (!this.canSubmit_()) { + this.showProblem_(MessageType.MISMATCH, ProblemType.ERROR); + this.enableSubmit = false; + // Focus the PIN keyboard and highlight the entire PIN. + this.$.pinKeyboard.focus(0, this.pinKeyboardValue_.length + 1); + return; + } + + assert(this.setModes); + this.setModes.call( + null, [chrome.quickUnlockPrivate.QuickUnlockMode.PIN], + [this.pinKeyboardValue_], this.onSetModesCompleted_.bind(this)); + this.writeUma(LockScreenProgress.CONFIRM_PIN); + }, + + /** + * @private + * @param {string} problemMessageId + * @param {string} problemClass + * @return {boolean} + */ + hasError_: function(problemMessageId, problemClass) { + return !!problemMessageId && problemClass == ProblemType.ERROR; + }, + + /** + * Formar problem message + * @private + * @param {string} locale i18n locale data + * @param {string} messageId + * @param {string} messageParameters + * @return {string} + */ + formatProblemMessage_: function(locale, messageId, messageParameters) { + return messageId ? this.i18nDynamic(locale, messageId, messageParameters) : + ''; + }, +}); + +})(); diff --git a/chromium/ui/webui/resources/cr_components/cr_components_images.grdp b/chromium/ui/webui/resources/cr_components/cr_components_images.grdp new file mode 100644 index 00000000000..9afe1f136f9 --- /dev/null +++ b/chromium/ui/webui/resources/cr_components/cr_components_images.grdp @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<grit-part> + <if expr="chromeos"> + <include name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_START_SETUP_ICON_1X_PNG" + file="cr_components/chromeos/multidevice_setup/start_setup_icon_1x.png" + type="BINDATA" + compress="gzip" /> + <include name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_START_SETUP_ICON_2X_PNG" + file="cr_components/chromeos/multidevice_setup/start_setup_icon_2x.png" + type="BINDATA" + compress="gzip" /> + <include name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_SUCCEEDED_ICON_1X_PNG" + file="cr_components/chromeos/multidevice_setup/setup_succeeded_icon_1x.png" + type="BINDATA" + compress="gzip" /> + <include name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_SUCCEEDED_ICON_2X_PNG" + file="cr_components/chromeos/multidevice_setup/setup_succeeded_icon_2x.png" + type="BINDATA" + compress="gzip" /> + </if> +</grit-part> diff --git a/chromium/ui/webui/resources/cr_components/cr_components_resources.grdp b/chromium/ui/webui/resources/cr_components/cr_components_resources.grdp index 2ff0de4098b..3bf1f1cdd56 100644 --- a/chromium/ui/webui/resources/cr_components/cr_components_resources.grdp +++ b/chromium/ui/webui/resources/cr_components/cr_components_resources.grdp @@ -123,19 +123,19 @@ file="cr_components/chromeos/network/network_choose_mobile.js" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_CONFIG_HTML" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_CONFIG_HTML" file="cr_components/chromeos/network/network_config.html" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_CONFIG_JS" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_CONFIG_JS" file="cr_components/chromeos/network/network_config.js" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_CONFIG_SELECT_HTML" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_CONFIG_SELECT_HTML" file="cr_components/chromeos/network/network_config_select.html" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_CONFIG_SELECT_JS" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_CONFIG_SELECT_JS" file="cr_components/chromeos/network/network_config_select.js" type="chrome_html" compress="gzip" /> @@ -155,11 +155,11 @@ file="cr_components/chromeos/network/network_nameservers.js" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_PASSWORD_INPUT_HTML" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_PASSWORD_INPUT_HTML" file="cr_components/chromeos/network/network_password_input.html" type="chrome_html" compress="gzip" /> - <structure name="IDR_WEBUI_CHROMEOS__NETWORK_PASSWORD_INPUT_JS" + <structure name="IDR_WEBUI_CHROMEOS_NETWORK_PASSWORD_INPUT_JS" file="cr_components/chromeos/network/network_password_input.js" type="chrome_html" compress="gzip" /> @@ -218,5 +218,127 @@ file="cr_components/chromeos/quick_unlock/pin_keyboard.js" type="chrome_html" compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_QUICK_UNLOCK_SETUP_PIN_KEYBOARD_JS" + file="cr_components/chromeos/quick_unlock/setup_pin_keyboard.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_QUICK_UNLOCK_SETUP_PIN_KEYBOARD_HTML" + file="cr_components/chromeos/quick_unlock/setup_pin_keyboard.html" + type="chrome_html" + compress="gzip"/> + <structure name="IDR_WEBUI_CHROMEOS_QUICK_UNLOCK_LOCK_SCREEN_CONSTANTS_JS" + file="cr_components/chromeos/quick_unlock/lock_screen_constants.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_QUICK_UNLOCK_LOCK_SCREEN_CONSTANTS_HTML" + file="cr_components/chromeos/quick_unlock/lock_screen_constants.html" + type="chrome_html" + compress="gzip" /> + + <!-- Shared between MultiDevice setup flow and OOBE. --> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_BROWSER_PROXY_HTML" + file="cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_BROWSER_PROXY_JS" + file="cr_components/chromeos/multidevice_setup/multidevice_setup_browser_proxy.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_BUTTON_BAR_HTML" + file="cr_components/chromeos/multidevice_setup/button_bar.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_BUTTON_BAR_JS" + file="cr_components/chromeos/multidevice_setup/button_bar.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_FAKE_MOJO_SERVICE_HTML" + file="cr_components/chromeos/multidevice_setup/fake_mojo_service.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_FAKE_MOJO_SERVICE_JS" + file="cr_components/chromeos/multidevice_setup/fake_mojo_service.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_ICONS_HTML" + file="cr_components/chromeos/multidevice_setup/icons.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MOJO_API_HTML" + file="cr_components/chromeos/multidevice_setup/mojo_api.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MOJO_API_JS" + file="cr_components/chromeos/multidevice_setup/mojo_api.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_HTML" + file="cr_components/chromeos/multidevice_setup/multidevice_setup.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_JS" + file="cr_components/chromeos/multidevice_setup/multidevice_setup.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_DELEGATE_HTML" + file="cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_DELEGATE_JS" + file="cr_components/chromeos/multidevice_setup/multidevice_setup_delegate.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_MULTIDEVICE_SETUP_SHARED_CSS_HTML" + file="cr_components/chromeos/multidevice_setup/multidevice_setup_shared_css.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_PASSWORD_PAGE_HTML" + file="cr_components/chromeos/multidevice_setup/password_page.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_PASSWORD_PAGE_JS" + file="cr_components/chromeos/multidevice_setup/password_page.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_FAILED_PAGE_HTML" + file="cr_components/chromeos/multidevice_setup/setup_failed_page.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_FAILED_PAGE_JS" + file="cr_components/chromeos/multidevice_setup/setup_failed_page.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_SUCCEEDED_PAGE_HTML" + file="cr_components/chromeos/multidevice_setup/setup_succeeded_page.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_SETUP_SUCCEEDED_PAGE_JS" + file="cr_components/chromeos/multidevice_setup/setup_succeeded_page.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_START_SETUP_PAGE_HTML" + file="cr_components/chromeos/multidevice_setup/start_setup_page.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_START_SETUP_PAGE_JS" + file="cr_components/chromeos/multidevice_setup/start_setup_page.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_UI_PAGE_CONTAINER_BEHAVIOR_HTML" + file="cr_components/chromeos/multidevice_setup/ui_page_container_behavior.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_UI_PAGE_CONTAINER_BEHAVIOR_JS" + file="cr_components/chromeos/multidevice_setup/ui_page_container_behavior.js" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_UI_PAGE_HTML" + file="cr_components/chromeos/multidevice_setup/ui_page.html" + type="chrome_html" + compress="gzip" /> + <structure name="IDR_WEBUI_CHROMEOS_MULTIDEVICE_SETUP_UI_PAGE_JS" + file="cr_components/chromeos/multidevice_setup/ui_page.js" + type="chrome_html" + compress="gzip" /> </if> </grit-part> diff --git a/chromium/ui/webui/resources/cr_elements/BUILD.gn b/chromium/ui/webui/resources/cr_elements/BUILD.gn index 90954848f2e..9927b6e60e8 100644 --- a/chromium/ui/webui/resources/cr_elements/BUILD.gn +++ b/chromium/ui/webui/resources/cr_elements/BUILD.gn @@ -22,6 +22,7 @@ group("closure_compile") { "cr_slider:closure_compile", "cr_toast:closure_compile", "cr_toggle:closure_compile", + "cr_view_manager:closure_compile", "policy:closure_compile", ] } diff --git a/chromium/ui/webui/resources/cr_elements/READE.md b/chromium/ui/webui/resources/cr_elements/README.md index b8b47ef98b3..b8b47ef98b3 100644 --- a/chromium/ui/webui/resources/cr_elements/READE.md +++ b/chromium/ui/webui/resources/cr_elements/README.md diff --git a/chromium/ui/webui/resources/cr_elements/chromeos/network/cr_network_select.js b/chromium/ui/webui/resources/cr_elements/chromeos/network/cr_network_select.js index 32dfbba4732..66036c7283d 100644 --- a/chromium/ui/webui/resources/cr_elements/chromeos/network/cr_network_select.js +++ b/chromium/ui/webui/resources/cr_elements/chromeos/network/cr_network_select.js @@ -159,6 +159,8 @@ Polymer({ if (this.cellularDeviceState_) this.ensureCellularNetwork_(networkStates); this.networkStateList_ = networkStates; + this.fire('network-list-changed', networkStates); + var defaultNetwork; for (var i = 0; i < networkStates.length; ++i) { var state = networkStates[i]; diff --git a/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.html b/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.html index 3bb931d9a4b..3c87967d159 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.html +++ b/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.html @@ -60,7 +60,7 @@ outline: none; } </style> - <dialog id="dialog" tabindex="0"> + <dialog id="dialog" tabindex="0" on-close="onNativeDialogClose_"> <div class="item-wrapper" tabindex="-1" role="menu"> <slot name="item" id="contentNode"></slot> </div> diff --git a/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.js b/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.js index ec1ac2d8fce..9c70ecede80 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.js +++ b/chromium/ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.js @@ -197,6 +197,24 @@ Polymer({ * @param {!Event} e * @private */ + onNativeDialogClose_: function(e) { + // Ignore any 'close' events not fired directly by the <dialog> element. + if (e.target !== this.$.dialog) + return; + + // TODO(dpapad): This is necessary to make the code work both for Polymer 1 + // and Polymer 2. Remove once migration to Polymer 2 is completed. + e.stopPropagation(); + + // Catch and re-fire the 'close' event such that it bubbles across Shadow + // DOM v1. + this.fire('close'); + }, + + /** + * @param {!Event} e + * @private + */ onTap_: function(e) { if (e.target == this) { this.close(); @@ -271,7 +289,7 @@ Polymer({ var options = this.querySelectorAll('.dropdown-item'); var numOptions = options.length; var focusedIndex = - Array.prototype.indexOf.call(options, this.root.activeElement); + Array.prototype.indexOf.call(options, getDeepActiveElement()); // Handle case where nothing is focused and up is pressed. if (focusedIndex === -1 && step === -1) diff --git a/chromium/ui/webui/resources/cr_elements/cr_container_shadow_behavior.js b/chromium/ui/webui/resources/cr_elements/cr_container_shadow_behavior.js index 45d8db171ec..3eb059ebfcf 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_container_shadow_behavior.js +++ b/chromium/ui/webui/resources/cr_elements/cr_container_shadow_behavior.js @@ -29,7 +29,7 @@ var CrContainerShadowBehavior = { var dropShadow = document.createElement('div'); // This ID should match the CSS rules in shared_styles_css.html. dropShadow.id = 'cr-container-shadow'; - this.shadowRoot.insertBefore(dropShadow, this.$.container); + this.$.container.parentNode.insertBefore(dropShadow, this.$.container); // Dummy element used to detect scrolling. Has a 0px height intentionally. var intersectionProbe = document.createElement('div'); diff --git a/chromium/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.js b/chromium/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.js index 864898edab5..ebf0ad52afa 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.js +++ b/chromium/ui/webui/resources/cr_elements/cr_dialog/cr_dialog.js @@ -52,6 +52,15 @@ Polymer({ }, /** + * True if the dialog should consume 'keydown' events. If ignoreEnterKey + * is true, 'Enter' key won't be consumed. + */ + consumeKeydownEvent: { + type: Boolean, + value: false, + }, + + /** * True if the dialog should not be able to be cancelled, which will prevent * 'Escape' key presses from closing the dialog. */ @@ -77,6 +86,9 @@ Polymer({ /** @private {?MutationObserver} */ mutationObserver_: null, + /** @private {?Function} */ + boundKeydown_: null, + /** @override */ ready: function() { // If the active history entry changes (i.e. user clicks back button), @@ -93,10 +105,13 @@ Polymer({ /** @override */ attached: function() { var mutationObserverCallback = function() { - if (this.$.dialog.open) + if (this.$.dialog.open) { this.addIntersectionObserver_(); - else + this.addKeydownListener_(); + } else { this.removeIntersectionObserver_(); + this.removeKeydownListener_(); + } }.bind(this); this.mutationObserver_ = new MutationObserver(mutationObserverCallback); @@ -113,6 +128,7 @@ Polymer({ /** @override */ detached: function() { this.removeIntersectionObserver_(); + this.removeKeydownListener_(); if (this.mutationObserver_) { this.mutationObserver_.disconnect(); this.mutationObserver_ = null; @@ -163,6 +179,31 @@ Polymer({ } }, + /** @private */ + addKeydownListener_: function() { + if (!this.consumeKeydownEvent) + return; + + this.boundKeydown_ = this.boundKeydown_ || this.onKeydown_.bind(this); + + this.addEventListener('keydown', this.boundKeydown_); + + // Sometimes <body> is key event's target and in that case the event + // will bypass cr-dialog. We should consume those events too in order to + // behave modally. This prevents accidentally triggering keyboard commands. + document.body.addEventListener('keydown', this.boundKeydown_); + }, + + /** @private */ + removeKeydownListener_: function() { + if (!this.boundKeydown_) + return; + + this.removeEventListener('keydown', this.boundKeydown_); + document.body.removeEventListener('keydown', this.boundKeydown_); + this.boundKeydown_ = null; + }, + showModal: function() { this.$.dialog.showModal(); assert(this.$.dialog.open); @@ -216,6 +257,10 @@ Polymer({ * @private */ onNativeDialogCancel_: function(e) { + // Ignore any 'cancel' events not fired directly by the <dialog> element. + if (e.target !== this.getNative()) + return; + if (this.noCancel) { e.preventDefault(); return; @@ -265,6 +310,23 @@ Polymer({ } }, + /** + * @param {!Event} e + * @private + */ + onKeydown_: function(e) { + assert(this.consumeKeydownEvent); + + if (!this.getNative().open) + return; + + if (this.ignoreEnterKey && e.key == 'Enter') + return; + + // Stop propagation to behave modally. + e.stopPropagation(); + }, + /** @param {!PointerEvent} e */ onPointerdown_: function(e) { // Only show pulse animation if user left-clicked outside of the dialog diff --git a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.html b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.html index 5be1ea9e8c9..b328ee4b72e 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.html +++ b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.html @@ -10,18 +10,22 @@ <template> <style include="cr-hidden-style cr-input-style"> /* + A 'suffix' element will be outside the underlined space, while a + 'prefix' element will be inside the underlined space by default. + Regarding cr-input's width: - When there's no element in the 'suffix' slot, setting the width of - cr-input as follows will work as expected: + When there's no element in the 'prefix' or 'suffix' slot, setting + the width of cr-input as follows will work as expected: cr-input { width: 200px; } - However, when there's an element in the 'suffix' slot, setting the - 'width' will dictate the total with of the input field *plus* the - 'suffix' element. To set the width of the input field itself when - a 'suffix' is present, use --cr-input-width. + However, when there's an element in the 'suffix' and/or 'prefix' + slot, setting the 'width' will dictate the total width of the input + field *plus* the 'prefix' and 'suffix' elements. To set the width + of the input field + 'prefix' when a 'suffix' is present, use + --cr-input-width. cr-input { --cr-input-width: 200px; @@ -72,8 +76,8 @@ color: var(--cr-input-error-color); display: var(--cr-input-error-display, block); font-size: var(--cr-form-field-label-font-size); - height: var(--cr-form-field-label-font-size); - line-height: var(--cr-form-field-label-font-size); + height: var(--cr-form-field-label-height); + line-height: var(--cr-form-field-label-line-height); margin: 8px 0; visibility: hidden; } @@ -82,14 +86,17 @@ visibility: visible; } - #row-container { + #row-container, + #inner-input-container { align-items: center; display: flex; /* This will spread the input field and the suffix apart only if the host element width is intentionally set to something large. */ justify-content: space-between; position: relative; + } + #row-container { @apply --cr-input-row-container; } @@ -103,12 +110,17 @@ <!-- Only attributes that are named inconsistently between html and js need to use attr$="", such as |tabindex| vs .tabIndex and |readonly| vs .readOnly. --> - <input id="input" disabled="[[disabled]]" autofocus="[[autofocus]]" - value="{{value::input}}" tabindex$="[[tabindex]]" type="[[type]]" - readonly$="[[readonly]]" maxlength$="[[maxlength]]" - pattern="[[pattern]]" required="[[required]]" - incremental="[[incremental]]" minlength$="[[minlength]]" - max="[[max]]" min="[[min]]"> + <div id="inner-input-container"> + <slot name="prefix"></slot> + <input id="input" disabled="[[disabled]]" autofocus="[[autofocus]]" + value="{{value::input}}" tabindex$="[[tabindex]]" type="[[type]]" + readonly$="[[readonly]]" maxlength$="[[maxlength]]" + pattern$="[[pattern]]" required="[[required]]" + minlength$="[[minlength]]" + max="[[max]]" min="[[min]]" on-focus="onInputFocus_" + on-blur="onInputBlur_" on-change="onInputChange_" + on-keydown="onInputKeydown_"> + </div> <div id="underline"></div> </div> <slot name="suffix"></slot> diff --git a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.js b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.js index 2cbd52aa289..67b9484a99b 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.js +++ b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input.js @@ -8,7 +8,6 @@ * Native input attributes that are currently supported by cr-inputs are: * autofocus * disabled - * incremental (only applicable when type="search") * max (only applicable when type="number") * min (only applicable when type="number") * maxlength @@ -72,8 +71,6 @@ Polymer({ reflectToAttribute: true, }, - incremental: Boolean, - invalid: { type: Boolean, value: false, @@ -150,11 +147,7 @@ Polymer({ }, listeners: { - 'input.focus': 'onInputFocus_', - 'input.blur': 'onInputBlur_', - 'input.change': 'onInputChange_', - 'input.keydown': 'onInputKeydown_', - 'focus': 'focusInput_', + 'focus': 'onFocus_', 'pointerdown': 'onPointerDown_', }, @@ -219,9 +212,24 @@ Polymer({ }, /** @private */ + onFocus_: function() { + if (!this.focusInput_()) + return; + // Always select the <input> element on focus. TODO(stevenjb/scottchen): + // Native <input> elements only do this for keyboard focus, not when + // focus() is called directly. Fix this? https://crbug.com/882612. + this.inputElement.select(); + }, + + /** + * @return {boolean} Whether the <input> element was focused. + * @private + */ focusInput_: function() { - if (this.shadowRoot.activeElement != this.inputElement) - this.inputElement.focus(); + if (this.shadowRoot.activeElement == this.inputElement) + return false; + this.inputElement.focus(); + return true; }, /** @private */ @@ -328,4 +336,4 @@ Polymer({ this.invalid = !this.inputElement.checkValidity(); return !this.invalid; }, -});
\ No newline at end of file +}); diff --git a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input_style_css.html b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input_style_css.html index 2b3e104397f..a78334b5223 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_input/cr_input_style_css.html +++ b/chromium/ui/webui/resources/cr_elements/cr_input/cr_input_style_css.html @@ -35,6 +35,15 @@ @apply --cr-input-container; } + #inner-input-container { + background-color: var(--cr-input-background-color, + var(--google-grey-refresh-100)); + box-sizing: border-box; + padding: 0; + + @apply --cr-input-inner-container; + } + #input { -webkit-appearance: none; background-color: var(--cr-input-background-color, @@ -78,11 +87,12 @@ opacity: 0; position: absolute; right: 0; - transition: opacity 120ms ease-out, width 0 linear 180ms; + transition: opacity 120ms ease-out, width 0s linear 180ms; width: 0; } :host([invalid]) #underline, + :host([force-underline]) #underline, :host([focused_]:not([readonly])) #underline { opacity: 1; transition: width 180ms ease-out, opacity 120ms ease-in; diff --git a/chromium/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js b/chromium/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js index 1c41e4a776f..179a09753c2 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js +++ b/chromium/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js @@ -32,6 +32,9 @@ var CrSearchFieldBehavior = { }, }, + /** @private {number} */ + searchDelayTimer_: -1, + /** * @return {!HTMLInputElement} The input field element the behavior should * use. @@ -59,6 +62,26 @@ var CrSearchFieldBehavior = { this.onValueChanged_(value, !!opt_noEvent); }, + /** @private */ + scheduleSearch_: function() { + if (this.searchDelayTimer_ >= 0) + clearTimeout(this.searchDelayTimer_); + // Dispatch 'search' event after: + // 0ms if the value is empty + // 500ms if the value length is 1 + // 400ms if the value length is 2 + // 300ms if the value length is 3 + // 200ms if the value length is 4 or greater. + // The logic here was copied from WebKit's native 'search' event. + var length = this.getValue().length; + var timeoutMs = length > 0 ? (500 - 100 * (Math.min(length, 4) - 1)) : 0; + this.searchDelayTimer_ = setTimeout(() => { + this.getSearchInput().dispatchEvent( + new CustomEvent('search', {composed: true, detail: this.getValue()})); + this.searchDelayTimer_ = -1; + }, timeoutMs); + }, + onSearchTermSearch: function() { this.onValueChanged_(this.getValue(), false); }, @@ -70,6 +93,7 @@ var CrSearchFieldBehavior = { */ onSearchTermInput: function() { this.hasSearchText = this.$.searchInput.value != ''; + this.scheduleSearch_(); }, /** diff --git a/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.html b/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.html index 168a5fec0e7..209dc714d37 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.html +++ b/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.html @@ -49,7 +49,8 @@ immediately as the user types unless the update-value-on-input flag is explicitly used. --> <cr-input label="[[label]]" on-click="onClick_" value="[[value]]" - on-input="onInput_" id="search" autofocus="[[autofocus]]"> + on-input="onInput_" id="search" autofocus="[[autofocus]]" + placeholder="[[placeholder]]"> </cr-input> <iron-dropdown horizontal-align="left" vertical-align="top" vertical-offset="52"> diff --git a/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.js b/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.js index 3ce90c6fe13..e7e94edb832 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.js +++ b/chromium/ui/webui/resources/cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.js @@ -20,6 +20,8 @@ Polymer({ reflectToAttribute: true, }, + placeholder: String, + /** @type {!Array<string>} */ items: Array, diff --git a/chromium/ui/webui/resources/cr_elements/cr_slider/BUILD.gn b/chromium/ui/webui/resources/cr_elements/cr_slider/BUILD.gn index 78d6d47bac9..ad5d8f69b5e 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_slider/BUILD.gn +++ b/chromium/ui/webui/resources/cr_elements/cr_slider/BUILD.gn @@ -12,6 +12,8 @@ js_type_check("closure_compile") { js_library("cr_slider") { deps = [ - "//third_party/polymer/v1_0/components-chromium/paper-slider:paper-slider-extracted", + "//third_party/polymer/v1_0/components-chromium/paper-behaviors:paper-ripple-behavior-extracted", + "//ui/webui/resources/js:cr", + "//ui/webui/resources/js:event_tracker", ] } diff --git a/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.html b/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.html index 05adf450012..f6203a2f110 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.html +++ b/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.html @@ -1,52 +1,197 @@ <link rel="import" href="../../html/polymer.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/paper-behaviors/paper-ripple-behavior.html"> +<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout.html"> +<link rel="import" href="../../html/cr.html"> +<link rel="import" href="../../html/event_tracker.html"> +<link rel="import" href="../hidden_style_css.html"> <link rel="import" href="../shared_vars_css.html"> -<link rel="import" href="chrome://resources/polymer/v1_0/paper-slider/paper-slider.html"> <dom-module id="cr-slider"> <template> - <style> - paper-slider { - --paper-slider-active-color: var(--google-blue-600); - --paper-slider-container-color: var(--google-blue-600-opacity-24); - --paper-slider-knob-color: var(--google-blue-600); - --paper-slider-knob-start-color: var(--google-blue-600); - --paper-slider-knob-start-border-color: var(--google-blue-600); - --paper-slider-pin-color: var(--google-blue-600); - --paper-slider-pin-start-color: var(--google-blue-600); - --paper-slider-markers-color: rgba(255, 255, 255, 0.54); - --paper-slider-disabled-active-color: var(--google-grey-600); - --paper-slider-disabled-knob-color: var(--google-grey-600); - width: 100%; - - --paper-slider-pin-text: { - font-family: Roboto; - font-size: 12px; - font-weight: 500; - line-height: 14px; - }; - } - - :host-context([dir=rtl]) paper-slider { - --paper-slider-pin-text: { - font-family: Roboto; - font-size: 12px; - font-weight: 500; - line-height: 14px; - transform: scale(-1, 1) translate(0, -17px); - }; - } - - paper-slider[disabled] { - --paper-slider-container-color: var(--google-grey-600-opacity-24); + <style include="cr-hidden-style"> + :host { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + cursor: default; + user-select: none; + } + + :host([dragging]) { + touch-action: none; + } + + #container { + height: 32px; + position: relative; + } + + #barContainer { + background-color: var(--google-blue-600-opacity-24); + border-radius: 1px; + height: 2px; + margin: 0 16px; + position: absolute; + top: 15px; + width: calc(100% - 32px); + } + + #bar { + background-color: var(--google-blue-600); + border-radius: 1px; + height: 2px; + left: 0; + position: absolute; + transition: width 80ms ease; + width: 0; + } + + :host-context([dir=rtl]) #bar { + left: initial; + right: 0; + } + + #knobContainer { + margin-inline-start: 12px; + position: absolute; + top: 11px; + width: calc(100% - 32px); + } + + #knob { + background-color: var(--google-blue-600); + border: 0; + border-radius: 50%; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.4); + height: 10px; + margin-inline-start: 0; + outline: none; + position: absolute; + transition: margin-inline-start 80ms ease; + width: 10px; + } + + paper-ripple { + color: var(--google-blue-600); + height: 32px; + left: -11px; + pointer-events: none; + top: -11px; + transition: color linear 80ms; + width: 32px; + } + + :host-context([dir=rtl]) paper-ripple { + left: auto; + right: -11px; + } + + #markers { + left: 0; + pointer-events: none; + position: absolute; + right: 0; + top: 0; + @apply --layout-horizontal; + } + + .active-marker, + .inactive-marker { + @apply --layout-flex; + } + #markers::before, + #markers::after, + .active-marker::after, + .inactive-marker::after { + border-radius: 50%; + content: ''; + display: block; + height: 2px; + margin-left: -1px; + width: 2px; + } + + #markers::before, + .active-marker::after { + background-color: rgba(255, 255, 255, 0.54); + } + + #markers::after, + .inactive-marker::after { + background-color: rgba(26, 115, 232, 0.54); + } + + #labelContainer { + cursor: default; + margin-inline-start: 1px; + opacity: 0; + transition: opacity 80ms ease-in-out; + user-select: none; + width: calc(100% - 32px); + } + + #container:hover #labelContainer, + .hover #labelContainer, + :host([hold-down_]) #labelContainer { + opacity: 1; + } + + #label { + background: var(--google-blue-600); + border-radius: 14px; + bottom: 28px; + color: white; + font-size: 12px; + line-height: 1.5em; + padding: 0 8px; + position: absolute; + transition: margin-inline-start 80ms ease; + white-space: nowrap; + } + + :host([disabled]) { + pointer-events: none; + } + + :host([disabled]) #barContainer { + background-color: var(--google-grey-600-opacity-24); + } + + :host([disabled]) #bar { + background-color: var(--google-grey-600); + } + + :host([disabled]) inactive-marker::after, + :host([disabled]) #markers::after { + background-color: rgba(255, 255, 255, 0.54); + } + + :host([disabled]) #knobContainer { + margin-inline-start: 9px; + top: 9px; + } + :host([disabled]) #knob { + background-color: var(--google-grey-600); + border: 2px solid white; + box-shadow: unset; } </style> - <paper-slider id="slider" - disabled$="[[disabled]]" snaps="[[snaps]]" on-change="onChange_" - max="[[max]]" min="[[min]]" on-up="resetTrackLock_" value="{{value}}" - max-markers="[[maxMarkers]]" immediate-value="{{immediateValue}}" - dragging="{{dragging}}"> - </paper-slider> + <div id="container"> + <div id="barContainer"> + <div id="bar"></div> + <div id="markers" hidden$="[[!markerCount]]"> + <template is="dom-repeat" items="[[getMarkers_(markerCount)]]"> + <div class$="[[getMarkerClass_(index, immediateValue_, min, max, + markerCount)]]"></div> + </template> + </div> + </div> + <div id="knobContainer"> + <div id="knob" tabindex="0"></div> + </div> + <div id="labelContainer" aria-label="[[label_]]"> + <div id="label">[[label_]]</div> + </div> + </div> </template> <script src="cr_slider.js"></script> </dom-module> diff --git a/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.js b/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.js index 16180c12ec0..97033f5ade8 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.js +++ b/chromium/ui/webui/resources/cr_elements/cr_slider/cr_slider.js @@ -3,148 +3,420 @@ // found in the LICENSE file. /** - * @fileoverview 'cr-slider' is a wrapper around paper-slider to alter the - * styling. The behavior of the slider remains the same. + * @fileoverview 'cr-slider' is a slider component used to select a number from + * a continuous or discrete range of numbers. */ -Polymer({ - is: 'cr-slider', - properties: { - min: Number, +cr.exportPath('cr_slider'); - max: Number, +/** + * The |value| is the corresponding value that the current slider tick is + * associated with. The string |label| is shown in the UI as the label for the + * current slider value. The |ariaValue| number is used for aria-valuemin, + * aria-valuemax, and aria-valuenow, and is optional. If missing, |value| will + * be used instead. + * @typedef {{ + * value: number, + * label: string, + * ariaValue: (number|undefined), + * }} + */ +cr_slider.SliderTick; + +(() => { + /** + * @param {number} min + * @param {number} max + * @param {number} value + * @return {number} + */ + function clamp(min, max, value) { + return Math.min(max, Math.max(min, value)); + } + + Polymer({ + is: 'cr-slider', + + behaviors: [ + Polymer.PaperRippleBehavior, + ], + + properties: { + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + + dragging: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + + markerCount: { + type: Number, + value: 0, + }, + + max: { + type: Number, + value: 100, + }, + + min: { + type: Number, + value: 0, + }, + + snaps: { + type: Boolean, + value: false, + }, + + /** + * The data associated with each tick on the slider. Each element in the + * array contains a value and the label corresponding to that value. + * @type {!Array<cr_slider.SliderTick>|!Array<number>} + */ + ticks: { + type: Array, + value: () => [], + observer: 'onTicksChanged_', + }, + + value: { + type: Number, + value: 0, + notify: true, + observer: 'onValueChanged_', + }, - snaps: { - type: Boolean, - value: true, + /** + * If true, |value| is updated while dragging happens. If false, |value| + * is updated only once, when drag gesture finishes. + */ + updateValueInstantly: { + type: Boolean, + value: true, + }, + + /** + * |immediateValue_| has the most up-to-date value and is used to render + * the slider UI. When dragging, |immediateValue_| is always updated, and + * |value| is updated at least once when dragging is stopped. + * @private + */ + immediateValue_: { + type: Number, + value: 0, + }, + + /** @private */ + holdDown_: { + type: Boolean, + value: false, + observer: 'onHoldDownChanged_', + reflectToAttribute: true, + }, + + /** @private */ + label_: { + type: String, + value: '', + }, }, - disabled: { - type: Boolean, - observer: 'onDisabledChanged_', + hostAttributes: { + role: 'slider', }, - value: Number, - maxMarkers: Number, + observers: [ + 'updateLabelAndAria_(immediateValue_, min, max)', + 'updateKnobAndBar_(immediateValue_, min, max)', + ], - immediateValue: { - type: Number, - observer: 'onImmediateValueChanged_', + listeners: { + focus: 'onFocus_', + blur: 'onBlur_', + keydown: 'onKeyDown_', + pointerdown: 'onPointerDown_', }, - dragging: Boolean, - }, + /** @private {Map<string, number>} */ + deltaKeyMap_: null, - listeners: { - 'focus': 'onFocus_', - 'blur': 'onBlur_', - 'keydown': 'onKeyDown_', - 'pointerdown': 'onPointerDown_', - 'pointerup': 'onPointerUp_', - }, + /** @private {boolean} */ + isRtl_: false, - /** @private {boolean} */ - usedMouse_: false, + /** @private {EventTracker} */ + draggingEventTracker_: null, - /** @override */ - attached: function() { - this.onDisabledChanged_(); - }, + /** @override */ + attached: function() { + this.isRtl_ = this.matches(':host-context([dir=rtl]) cr-slider'); + this.deltaKeyMap_ = new Map([ + ['ArrowDown', -1], + ['ArrowUp', 1], + ['PageDown', -1], + ['PageUp', 1], + ['ArrowLeft', this.isRtl_ ? 1 : -1], + ['ArrowRight', this.isRtl_ ? -1 : 1], + ]); + this.draggingEventTracker_ = new EventTracker(); + }, - /** @private */ - onFocus_: function() { - this.$.slider.getRipple().holdDown = true; - this.$.slider._expandKnob(); - }, + /** + * When markers are displayed on the slider, they are evenly spaced across + * the entire slider bar container and are rendered on top of the bar and + * bar container. The location of the marks correspond to the discrete + * values that the slider can have. + * @return {!Array} The array items have no type since this is used to + * create |markerCount| number of markers. + * @private + */ + getMarkers_: function() { + return new Array(Math.max(0, this.markerCount - 1)); + }, - /** @private */ - onBlur_: function() { - this.$.slider.getRipple().holdDown = false; - this.$.slider._resetKnob(); - }, + /** + * @param {number} index + * @return {string} + * @private + */ + getMarkerClass_: function(index) { + const currentStep = (this.markerCount - 1) * this.getRatio_(); + return index < currentStep ? 'active-marker' : 'inactive-marker'; + }, - /** @private */ - onChange_: function() { - this.$.slider._setExpand(!this.usedMouse_); - this.$.slider.getRipple().holdDown = !this.usedMouse_; - this.usedMouse_ = false; - }, + /** + * The ratio is a value from 0 to 1.0 corresponding to a location along the + * slider bar where 0 is the minimum value and 1.0 is the maximum value. + * This is a helper function used to calculate the bar width, knob location + * and label location. + * @return {number} + * @private + */ + getRatio_: function() { + return (this.immediateValue_ - this.min) / (this.max - this.min); + }, - /** @private */ - onKeyDown_: function() { - this.usedMouse_ = false; - if (!this.disabled) - this.onFocus_(); - }, + /** @private */ + ensureValidValue_: function() { + if (this.immediateValue_ == undefined || this.value == undefined) + return; + let validValue = clamp(this.min, this.max, this.immediateValue_); + validValue = this.snaps ? Math.round(validValue) : validValue; + this.immediateValue_ = validValue; + if (!this.dragging || this.updateValueInstantly) + this.value = validValue; + }, - /** - * @param {!MouseEvent} event - * @private - */ - onPointerDown_: function(event) { - if (this.disabled || event.button != 0) { - event.preventDefault(); - return; - } - this.usedMouse_ = true; - setTimeout(() => { - this.$.slider.getRipple().holdDown = true; - }); - }, + /** + * Removes all event listeners related to dragging, and cancels ripple. + * @param {number} pointerId + * @private + */ + stopDragging_: function(pointerId) { + this.dragging = false; + this.draggingEventTracker_.removeAll(); + this.value = this.immediateValue_; + // If there is a ripple animation in progress, setTimeout will hold off + // on updating |holdDown_|. + setTimeout(() => { + this.holdDown_ = false; + }); + this.releasePointerCapture(pointerId); + }, - /** - * @param {!MouseEvent} event - * @private - */ - onPointerUp_: function(event) { - if (event.button != 0) - return; - this.$.slider.getRipple().holdDown = false; - }, + /** @private */ + onBlur_: function() { + this.holdDown_ = false; + }, - /** - * The style is being set in this way to keep the knob size the same - * regardless of the state or properties set in the paper-slider. paper-slider - * styles alter the size in multiple places making it difficult to introduce - * one or two mixins to override the existing paper-slider knob styling. - * @private - */ - onDisabledChanged_: function() { - const knob = this.$.slider.$$('.slider-knob-inner'); - knob.style.boxSizing = 'content-box'; - knob.style.height = '10px'; - knob.style.transform = 'unset'; - knob.style.transition = 'unset'; - knob.style.width = '10px'; - this.$.slider.$$('.bar-container').style.left = '0'; - if (this.disabled) { - knob.style.backgroundColor = 'var(--google-grey-600)'; - knob.style.border = '2px solid white'; - knob.style.boxShadow = 'unset'; - knob.style.margin = '9px'; - } else { - knob.style.backgroundColor = 'var(--google-blue-600)'; - knob.style.border = '0'; - knob.style.boxShadow = '0 1px 3px 0 rgba(0, 0, 0, 0.4)'; - knob.style.margin = '11px'; - } - }, - - /** @private */ - onImmediateValueChanged_: function() { - // TODO(dpapad): Need to catch and refire the property changed event in - // Polymer 2 only, since it does not bubble by default. Remove the - // condition when migration to Polymer 2 is completed. - if (Polymer.DomIf) - this.fire('immediate-value-changed', this.immediateValue); - }, + /** @private */ + onFocus_: function() { + this.holdDown_ = true; + }, - /** - * TODO(scottchen): temporary fix until polymer gesture bug resolved. See: - * https://github.com/PolymerElements/paper-slider/issues/186 - * @private - */ - resetTrackLock_: function() { - Polymer.Gestures.gestures.tap.reset(); - }, -}); + /** @private */ + onHoldDownChanged_: function() { + this.getRipple().holdDown = this.holdDown_; + }, + + /** + * @param {!Event} event + * @private + */ + onKeyDown_: function(event) { + if (this.disabled) + return; + + if (event.metaKey || event.shiftKey || event.altKey || event.ctrlKey) + return; + + let handled = true; + if (event.key == 'Home') + this.value = this.min; + else if (event.key == 'End') + this.value = this.max; + else if (this.deltaKeyMap_.has(event.key)) { + const newValue = this.value + this.deltaKeyMap_.get(event.key); + this.value = clamp(this.min, this.max, newValue); + } else + handled = false; + + if (handled) { + event.preventDefault(); + setTimeout(() => { + this.holdDown_ = true; + }); + } + }, + + /** + * When the left-mouse button is pressed, the knob location is updated and + * dragging starts. + * @param {!PointerEvent} event + * @private + */ + onPointerDown_: function(event) { + if (this.disabled || event.buttons != 1 && event.pointerType == 'mouse') + return; + + this.dragging = true; + // If there is a ripple animation in progress, setTimeout will hold off on + // updating |holdDown_|. + setTimeout(() => { + this.$.knob.focus(); + this.holdDown_ = true; + }); + this.updateValueFromClientX_(event.clientX); + + this.setPointerCapture(event.pointerId); + const stopDragging = this.stopDragging_.bind(this, event.pointerId); + + this.draggingEventTracker_.add(this, 'pointermove', e => { + // If the left-button on the mouse is pressed by itself, then update. + // Otherwise stop capturing the mouse events because the drag operation + // is complete. + if (e.buttons != 1 && e.pointerType == 'mouse') { + stopDragging(); + return; + } + this.updateValueFromClientX_(e.clientX); + }); + this.draggingEventTracker_.add(this, 'pointercancel', stopDragging); + this.draggingEventTracker_.add(this, 'pointerdown', stopDragging); + this.draggingEventTracker_.add(this, 'pointerup', stopDragging); + this.draggingEventTracker_.add(this, 'keydown', e => { + if (e.key == 'Escape' || e.key == 'Tab') + stopDragging(); + }); + }, + + /** @private */ + onTicksChanged_: function() { + if (this.ticks.length == 0) { + this.disabled = false; + this.snaps = false; + } else if (this.ticks.length == 1) { + this.disabled = true; + } else { + this.disabled = false; + this.snaps = true; + this.max = this.ticks.length - 1; + this.min = 0; + } + this.ensureValidValue_(); + this.updateLabelAndAria_(); + }, + + /** + * Update |immediateValue_| which is used for rendering when |value| is + * updated either programmatically or from a keyboard input or a mouse drag + * (when |updateValueInstantly| is true). + * @private + */ + onValueChanged_: function() { + if (this.immediateValue_ == this.value) + return; + + this.immediateValue_ = this.value; + this.ensureValidValue_(); + }, + + /** @private */ + updateKnobAndBar_: function() { + const percent = `${this.getRatio_() * 100}%`; + this.$.bar.style.width = percent; + this.$.knob.style.marginInlineStart = percent; + }, + + /** @private */ + updateLabelAndAria_: function() { + const ticks = this.ticks; + const index = this.immediateValue_; + if (!ticks || ticks.length == 0 || index >= ticks.length || + !Number.isInteger(index) || !this.snaps) { + this.setAttribute('aria-valuetext', index); + this.setAttribute('aria-valuemin', this.min); + this.setAttribute('aria-valuemax', this.max); + this.setAttribute('aria-valuenow', index); + return; + } + const tick = ticks[index]; + this.label_ = Number.isFinite(tick) ? '' : tick.label; + + // Update label location after it has been rendered. + this.async(() => { + const label = this.$.label; + const parentWidth = label.parentElement.offsetWidth; + const labelWidth = label.offsetWidth; + // The left and right margin are 16px. + const margin = 16; + const knobLocation = parentWidth * this.getRatio_() + margin; + const offsetStart = knobLocation - (labelWidth / 2); + // The label should be centered over the knob. Clamping the offset to a + // min and max value prevents the label from being cutoff. + const max = parentWidth + 2 * margin - labelWidth; + label.style.marginInlineStart = + `${Math.round(clamp(0, max, offsetStart))}px`; + }); + + const ariaValues = [tick, ticks[0], ticks[ticks.length - 1]].map(t => { + if (Number.isFinite(t)) + return t; + return Number.isFinite(t.ariaValue) ? t.ariaValue : t.value; + }); + this.setAttribute( + 'aria-valuetext', + this.label_.length > 0 ? this.label_ : ariaValues[0]); + this.setAttribute('aria-valuenow', ariaValues[0]); + this.setAttribute('aria-valuemin', ariaValues[1]); + this.setAttribute('aria-valuemax', ariaValues[2]); + }, + + /** + * @param {number} clientX + * @private + */ + updateValueFromClientX_: function(clientX) { + const rect = this.$.barContainer.getBoundingClientRect(); + let ratio = (clientX - rect.left) / rect.width; + if (this.isRtl_) + ratio = 1 - ratio; + this.immediateValue_ = ratio * (this.max - this.min) + this.min; + this.ensureValidValue_(); + }, + + _createRipple: function() { + this._rippleContainer = this.$.knob; + const ripple = Polymer.PaperRippleBehavior._createRipple(); + ripple.id = 'ink'; + ripple.setAttribute('recenters', ''); + ripple.classList.add('circle', 'toggle-ink'); + return ripple; + }, + }); +})(); diff --git a/chromium/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html b/chromium/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html index 635bfe77f64..0f03fdf8a69 100644 --- a/chromium/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html +++ b/chromium/ui/webui/resources/cr_elements/cr_toolbar/cr_toolbar_search_field.html @@ -150,7 +150,6 @@ on-keydown="onSearchTermKeydown_" on-focus="onInputFocus_" on-blur="onInputBlur_" - incremental autofocus spellcheck="false"> </div> diff --git a/chromium/ui/webui/resources/cr_elements/cr_view_manager/BUILD.gn b/chromium/ui/webui/resources/cr_elements/cr_view_manager/BUILD.gn new file mode 100644 index 00000000000..e1de4429a50 --- /dev/null +++ b/chromium/ui/webui/resources/cr_elements/cr_view_manager/BUILD.gn @@ -0,0 +1,19 @@ +# Copyright 2018 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//third_party/closure_compiler/compile_js.gni") + +js_type_check("closure_compile") { + deps = [ + ":cr_view_manager", + ] +} + +js_library("cr_view_manager") { + deps = [ + "//ui/webui/resources/js:assert", + "//ui/webui/resources/js:cr", + ] + externs_list = [ "$externs_path/web_animations.js" ] +} diff --git a/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.html b/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.html new file mode 100644 index 00000000000..45ff7d81938 --- /dev/null +++ b/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.html @@ -0,0 +1,26 @@ +<link rel="import" href="../../html/polymer.html"> + +<link rel="import" href="../../html/assert.html"> +<link rel="import" href="../../html/cr.html"> + +<dom-module id="cr-view-manager"> + <template> + <style> + :host ::slotted([slot=view]) { + bottom: 0; + display: none; + left: 0; + position: absolute; + right: 0; + top: 0; + } + + :host ::slotted(.active), + :host ::slotted(.closing) { + display: block; + } + </style> + <slot name="view"></slot> + </template> + <script src="cr_view_manager.js"></script> +</dom-module> diff --git a/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.js b/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.js new file mode 100644 index 00000000000..44f67b9877d --- /dev/null +++ b/chromium/ui/webui/resources/cr_elements/cr_view_manager/cr_view_manager.js @@ -0,0 +1,115 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +(function() { +/** + * TODO(scottchen): shim for not having Animation.finished implemented. Can + * replace with Animation.finished if Chrome implements it (see: + * crbug.com/257235). + * @param {!Animation} animation + * @return {!Promise} + */ +function whenFinished(animation) { + return new Promise(function(resolve, reject) { + animation.addEventListener('finish', resolve); + }); +} + +/** @type {!Map<string, function(!Element): !Promise>} */ +const viewAnimations = new Map(); +viewAnimations.set('no-animation', () => Promise.resolve()); +viewAnimations.set('fade-in', element => { + const animation = element.animate( + { + opacity: [0, 1], + }, + /** @type {!KeyframeEffectOptions} */ ({ + duration: 180, + easing: 'ease-in-out', + iterations: 1, + })); + + return whenFinished(animation); +}); +viewAnimations.set('fade-out', element => { + const animation = element.animate( + { + opacity: [1, 0], + }, + /** @type {!KeyframeEffectOptions} */ ({ + duration: 180, + easing: 'ease-in-out', + iterations: 1, + })); + + return whenFinished(animation); +}); + +Polymer({ + is: 'cr-view-manager', + + /** + * @param {!Element} element + * @param {string} animation + * @return {!Promise} + * @private + */ + exit_: function(element, animation) { + const animationFunction = viewAnimations.get(animation); + assert(animationFunction); + + element.classList.remove('active'); + element.classList.add('closing'); + element.dispatchEvent( + new CustomEvent('view-exit-start', {bubbles: true, composed: true})); + return animationFunction(element).then(function() { + element.classList.remove('closing'); + element.dispatchEvent( + new CustomEvent('view-exit-finish', {bubbles: true, composed: true})); + }); + }, + + /** + * @param {!Element} view + * @param {string} animation + * @return {!Promise} + * @private + */ + enter_: function(view, animation) { + const animationFunction = viewAnimations.get(animation); + assert(animationFunction); + + let effectiveView = view.matches('cr-lazy-render') ? view.get() : view; + + effectiveView.classList.add('active'); + effectiveView.dispatchEvent( + new CustomEvent('view-enter-start', {bubbles: true, composed: true})); + return animationFunction(effectiveView).then(() => { + effectiveView.dispatchEvent(new CustomEvent( + 'view-enter-finish', {bubbles: true, composed: true})); + }); + }, + + /** + * @param {string} newViewId + * @param {string=} enterAnimation + * @param {string=} exitAnimation + * @return {!Promise} + */ + switchView: function(newViewId, enterAnimation, exitAnimation) { + const previousView = this.querySelector('.active'); + const newView = assert(this.querySelector('#' + newViewId)); + + const promises = []; + if (previousView) { + promises.push(this.exit_(previousView, exitAnimation || 'fade-out')); + promises.push(this.enter_(newView, enterAnimation || 'fade-in')); + } else { + promises.push(this.enter_(newView, 'no-animation')); + } + + return Promise.all(promises); + }, +}); +})();
\ No newline at end of file diff --git a/chromium/ui/webui/resources/cr_elements/policy/BUILD.gn b/chromium/ui/webui/resources/cr_elements/policy/BUILD.gn index 244f3a615eb..743f98969f3 100644 --- a/chromium/ui/webui/resources/cr_elements/policy/BUILD.gn +++ b/chromium/ui/webui/resources/cr_elements/policy/BUILD.gn @@ -54,6 +54,7 @@ js_library("cr_policy_network_indicator") { deps = [ ":cr_policy_indicator_behavior", ":cr_policy_network_behavior", + ":cr_tooltip_icon", "../chromeos/network:cr_onc_types", ] } diff --git a/chromium/ui/webui/resources/cr_elements/policy/cr_policy_network_behavior.js b/chromium/ui/webui/resources/cr_elements/policy/cr_policy_network_behavior.js index eb89b2b66db..c20b1ca4ec6 100644 --- a/chromium/ui/webui/resources/cr_elements/policy/cr_policy_network_behavior.js +++ b/chromium/ui/webui/resources/cr_elements/policy/cr_policy_network_behavior.js @@ -53,22 +53,42 @@ var CrPolicyNetworkBehavior = { /** * @param {!CrOnc.ManagedProperty|undefined} property - * @return {boolean} True if the network property is enforced by a policy. + * @return {boolean} True if the network property is editable. */ - isNetworkPolicyEnforced: function(property) { - if (!this.isNetworkPolicyControlled(property)) + isEditable: function(property) { + // If the property is not a dictionary, then the property is not editable. + if (typeof property != 'object') return false; + // If the property has a UserEditable sub-property, that determines whether - // or not it is editable (not enforced). + // or not it is editable. if (typeof property.UserEditable != 'undefined') - return !property.UserEditable; + return property.UserEditable; // Otherwise if the property has a DeviceEditable sub-property, check that. if (typeof property.DeviceEditable != 'undefined') - return !property.DeviceEditable; + return property.DeviceEditable; + + // If no 'Editable' sub-property exists, the policy value is not editable. + return false; + }, + + /** + * @param {!CrOnc.ManagedProperty|undefined} property + * @return {boolean} True if the network property is enforced by a policy. + */ + isNetworkPolicyEnforced: function(property) { + return this.isNetworkPolicyControlled(property) && + !this.isEditable(property); + }, - // If no 'Editable' sub-property exists, the policy value is enforced. - return true; + /** + * @param {!CrOnc.ManagedProperty|undefined} property + * @return {boolean} True if the network property is recommended by a policy. + */ + isNetworkPolicyRecommended: function(property) { + return this.isNetworkPolicyControlled(property) && + this.isEditable(property); }, /** diff --git a/chromium/ui/webui/resources/cr_elements/policy/cr_policy_pref_indicator.js b/chromium/ui/webui/resources/cr_elements/policy/cr_policy_pref_indicator.js index 447aa7fb810..b83f94971b0 100644 --- a/chromium/ui/webui/resources/cr_elements/policy/cr_policy_pref_indicator.js +++ b/chromium/ui/webui/resources/cr_elements/policy/cr_policy_pref_indicator.js @@ -78,4 +78,9 @@ Polymer({ return this.getIndicatorTooltip( indicatorType, this.pref.controlledByName || '', matches); }, + + /** @return {!Element} */ + getFocusableElement: function() { + return this.$$('cr-tooltip-icon').getFocusableElement(); + }, }); diff --git a/chromium/ui/webui/resources/cr_elements/policy/cr_tooltip_icon.js b/chromium/ui/webui/resources/cr_elements/policy/cr_tooltip_icon.js index ba918fb0ca7..cd8cd9cc2ab 100644 --- a/chromium/ui/webui/resources/cr_elements/policy/cr_tooltip_icon.js +++ b/chromium/ui/webui/resources/cr_elements/policy/cr_tooltip_icon.js @@ -9,4 +9,9 @@ Polymer({ iconClass: String, tooltipText: String, }, + + /** @return {!Element} */ + getFocusableElement: function() { + return this.$.indicator; + }, });
\ No newline at end of file diff --git a/chromium/ui/webui/resources/cr_elements/shared_vars_css.html b/chromium/ui/webui/resources/cr_elements/shared_vars_css.html index b98339dcb19..f774f420268 100644 --- a/chromium/ui/webui/resources/cr_elements/shared_vars_css.html +++ b/chromium/ui/webui/resources/cr_elements/shared_vars_css.html @@ -142,13 +142,15 @@ --cr-disabled-opacity: 0.38; --cr-form-field-bottom-spacing: 16px; --cr-form-field-label-font-size: 0.625rem; + --cr-form-field-label-height: 0.625rem; + --cr-form-field-label-line-height: 0.625rem; --cr-form-field-label: { color: var(--google-grey-refresh-700); display: block; font-size: var(--cr-form-field-label-font-size); font-weight: 500; letter-spacing: 0.4px; - line-height: var(--cr-form-field-label-font-size); + line-height: var(--cr-form-field-label-line-height); margin-bottom: 8px; } --google-blue-50: #E8F0FE; diff --git a/chromium/ui/webui/resources/cr_elements_resources.grdp b/chromium/ui/webui/resources/cr_elements_resources.grdp index 26c467315df..040b089fbef 100644 --- a/chromium/ui/webui/resources/cr_elements_resources.grdp +++ b/chromium/ui/webui/resources/cr_elements_resources.grdp @@ -126,6 +126,12 @@ file="cr_elements/cr_searchable_drop_down/cr_searchable_drop_down.js" type="chrome_html" compress="gzip" /> + <structure name="IDR_CR_ELEMENTS_CR_VIEW_MANAGER_HTML" + file="cr_elements/cr_view_manager/cr_view_manager.html" + type="chrome_html" /> + <structure name="IDR_CR_ELEMENTS_CR_VIEW_MANAGER_JS" + file="cr_elements/cr_view_manager/cr_view_manager.js" + type="chrome_html" /> <if expr="chromeos"> <structure name="IDR_CR_ELEMENTS_CHROMEOS_CR_PICTURE_CR_CAMERA_HTML" file="cr_elements/chromeos/cr_picture/cr_camera.html" diff --git a/chromium/ui/webui/resources/js/assert.js b/chromium/ui/webui/resources/js/assert.js index 42fb523fb07..e46a056778e 100644 --- a/chromium/ui/webui/resources/js/assert.js +++ b/chromium/ui/webui/resources/js/assert.js @@ -22,6 +22,8 @@ function assert(condition, opt_message) { message = message + ': ' + opt_message; var error = new Error(message); var global = function() { + /** @type {boolean} */ + this.traceAssertionsForTesting; return this; }(); if (global.traceAssertionsForTesting) diff --git a/chromium/ui/webui/resources/js/cr/ui/dialogs.js b/chromium/ui/webui/resources/js/cr/ui/dialogs.js index 40e86f6c368..a2aff076194 100644 --- a/chromium/ui/webui/resources/js/cr/ui/dialogs.js +++ b/chromium/ui/webui/resources/js/cr/ui/dialogs.js @@ -19,6 +19,9 @@ cr.define('cr.ui.dialogs', function() { this.previousActiveElement_ = null; this.initDom_(); + + /** @private{boolean} */ + this.showing_ = false; } /** @@ -214,6 +217,7 @@ cr.define('cr.ui.dialogs', function() { */ BaseDialog.prototype.show_ = function( title, opt_onOk, opt_onCancel, opt_onShow) { + this.showing_ = true; // Make all outside nodes unfocusable while the dialog is active. this.deactivatedNodes_ = this.findFocusableElements_(this.document_); this.tabIndexes_ = this.deactivatedNodes_.map(function(n) { @@ -239,10 +243,11 @@ cr.define('cr.ui.dialogs', function() { var self = this; setTimeout(function() { - // Note that we control the opacity of the *container*, but the top/left - // of the *frame*. - self.container_.classList.add('shown'); - self.initialFocusElement_.focus(); + // Check that hide() was not called in between. + if (self.showing_) { + self.container_.classList.add('shown'); + self.initialFocusElement_.focus(); + } setTimeout(function() { if (opt_onShow) opt_onShow(); @@ -252,6 +257,7 @@ cr.define('cr.ui.dialogs', function() { /** @param {Function=} opt_onHide */ BaseDialog.prototype.hide = function(opt_onHide) { + this.showing_ = false; // Restore focusability. for (var i = 0; i < this.deactivatedNodes_.length; i++) { var node = this.deactivatedNodes_[i]; @@ -263,8 +269,6 @@ cr.define('cr.ui.dialogs', function() { this.deactivatedNodes_ = null; this.tabIndexes_ = null; - // Note that we control the opacity of the *container*, but the top/left - // of the *frame*. this.container_.classList.remove('shown'); if (this.previousActiveElement_) { @@ -277,9 +281,10 @@ cr.define('cr.ui.dialogs', function() { var self = this; setTimeout(function() { // Wait until the transition is done before removing the dialog. - // It is possible to show/hide/show/hide and have hide called twice + // Check show() was not called in between. + // It is also possible to show/hide/show/hide and have hide called twice // and container_ already removed from parentNode_. - if (self.parentNode_ === self.container_.parentNode) + if (!self.showing_ && self.parentNode_ === self.container_.parentNode) self.parentNode_.removeChild(self.container_); if (opt_onHide) opt_onHide(); diff --git a/chromium/ui/webui/resources/js/cr/ui/focus_grid.js b/chromium/ui/webui/resources/js/cr/ui/focus_grid.js index 000fb051966..de710112a36 100644 --- a/chromium/ui/webui/resources/js/cr/ui/focus_grid.js +++ b/chromium/ui/webui/resources/js/cr/ui/focus_grid.js @@ -25,7 +25,7 @@ cr.define('cr.ui', function() { * focusable focusable focusable * * @constructor - * @implements {cr.ui.FocusRow.Delegate} + * @implements {cr.ui.FocusRowDelegate} */ function FocusGrid() { /** @type {!Array<!cr.ui.FocusRow>} */ diff --git a/chromium/ui/webui/resources/js/cr/ui/focus_row.js b/chromium/ui/webui/resources/js/cr/ui/focus_row.js index 91399fb5250..8915b114a4a 100644 --- a/chromium/ui/webui/resources/js/cr/ui/focus_row.js +++ b/chromium/ui/webui/resources/js/cr/ui/focus_row.js @@ -14,84 +14,76 @@ cr.define('cr.ui', function() { * If no items in this row are focused, the row can stay active until focus * changes to a node inside |this.boundary_|. If |boundary| isn't specified, * any focus change deactivates the row. - * - * @param {!Element} root The root of this focus row. Focus classes are - * applied to |root| and all added elements must live within |root|. - * @param {?Element} boundary Focus events are ignored outside of this - * element. - * @param {cr.ui.FocusRow.Delegate=} opt_delegate An optional event delegate. - * @constructor */ - function FocusRow(root, boundary, opt_delegate) { - /** @type {!Element} */ - this.root = root; - - /** @private {!Element} */ - this.boundary_ = boundary || document.documentElement; - - /** @type {cr.ui.FocusRow.Delegate|undefined} */ - this.delegate = opt_delegate; - - /** @protected {!EventTracker} */ - this.eventTracker = new EventTracker; - } - - /** @interface */ - FocusRow.Delegate = function() {}; - - FocusRow.Delegate.prototype = { + class FocusRow { /** - * Called when a key is pressed while on a FocusRow's item. If true is - * returned, further processing is skipped. - * @param {!cr.ui.FocusRow} row The row that detected a keydown. - * @param {!Event} e - * @return {boolean} Whether the event was handled. + * @param {!Element} root The root of this focus row. Focus classes are + * applied to |root| and all added elements must live within |root|. + * @param {?Element} boundary Focus events are ignored outside of this + * element. + * @param {cr.ui.FocusRowDelegate=} delegate An optional event + * delegate. */ - onKeydown: assertNotReached, + constructor(root, boundary, delegate) { + /** @type {!Element} */ + this.root = root; - /** - * @param {!cr.ui.FocusRow} row - * @param {!Event} e - */ - onFocus: assertNotReached, - }; + /** @private {!Element} */ + this.boundary_ = boundary || document.documentElement; - /** @const {string} */ - FocusRow.ACTIVE_CLASS = 'focus-row-active'; + /** @type {cr.ui.FocusRowDelegate|undefined} */ + this.delegate = delegate; - /** - * Whether it's possible that |element| can be focused. - * @param {Element} element - * @return {boolean} Whether the item is focusable. - */ - FocusRow.isFocusable = function(element) { - if (!element || element.disabled) - return false; + /** @protected {!EventTracker} */ + this.eventTracker = new EventTracker; + } - // We don't check that element.tabIndex >= 0 here because inactive rows set - // a tabIndex of -1. + /** + * Whether it's possible that |element| can be focused. + * @param {Element} element + * @return {boolean} Whether the item is focusable. + */ + static isFocusable(element) { + if (!element || element.disabled) + return false; - function isVisible(element) { - assertInstanceof(element, Element); + // We don't check that element.tabIndex >= 0 here because inactive rows + // set a tabIndex of -1. + let current = element; + while (true) { + assertInstanceof(current, Element); - var style = window.getComputedStyle(element); - if (style.visibility == 'hidden' || style.display == 'none') - return false; + var style = window.getComputedStyle(current); + if (style.visibility == 'hidden' || style.display == 'none') + return false; - var parent = element.parentNode; - if (!parent) - return false; + var parent = current.parentNode; + if (!parent) + return false; - if (parent == element.ownerDocument || parent instanceof DocumentFragment) - return true; + if (parent == current.ownerDocument || + parent instanceof DocumentFragment) + return true; - return isVisible(parent); + current = /** @type {Element} */ (parent); + } } - return isVisible(element); - }; + /** + * A focus override is a function that returns an element that should gain + * focus. The element may not be directly selectable for example the element + * that can gain focus is in a shadow DOM. Allowing an override via a + * function leaves the details of how the element is retrieved to the + * component. + * @param {!Element} element + * @return {!Element} + */ + static getFocusableElement(element) { + if (element.getFocusableElement) + return element.getFocusableElement(); + return element; + } - FocusRow.prototype = { /** * Register a new type of focusable element (or add to an existing one). * @@ -100,7 +92,7 @@ cr.define('cr.ui', function() { * When FocusRow is used within a FocusGrid, these types are used to * determine equivalent controls when Up/Down are pressed to change rows. * - * Another example: mutually exclusive controls that hide eachother on + * Another example: mutually exclusive controls that hide each other on * activation (i.e. Play/Pause) could use the same type (i.e. 'play-pause') * to indicate they're equivalent. * @@ -109,7 +101,7 @@ cr.define('cr.ui', function() { * from this row's root, or the element itself. * @return {boolean} Whether a new item was added. */ - addItem: function(type, selectorOrElement) { + addItem(type, selectorOrElement) { assert(type); var element; @@ -128,30 +120,30 @@ cr.define('cr.ui', function() { this.eventTracker.add(element, 'keydown', this.onKeydown_.bind(this)); this.eventTracker.add(element, 'mousedown', this.onMousedown_.bind(this)); return true; - }, + } /** Dereferences nodes and removes event handlers. */ - destroy: function() { + destroy() { this.eventTracker.removeAll(); - }, + } /** * @param {!Element} sampleElement An element for to find an equivalent for. * @return {!Element} An equivalent element to focus for |sampleElement|. * @protected */ - getCustomEquivalent: function(sampleElement) { + getCustomEquivalent(sampleElement) { return assert(this.getFirstFocusable()); - }, + } /** * @return {!Array<!Element>} All registered elements (regardless of * focusability). */ - getElements: function() { - var elements = this.root.querySelectorAll('[focus-type]'); - return Array.prototype.slice.call(elements); - }, + getElements() { + return Array.from(this.root.querySelectorAll('[focus-type]')) + .map(cr.ui.FocusRow.getFocusableElement); + } /** * Find the element that best matches |sampleElement|. @@ -159,7 +151,7 @@ cr.define('cr.ui', function() { * which previously held focus. * @return {!Element} The element that best matches sampleElement. */ - getEquivalentElement: function(sampleElement) { + getEquivalentElement(sampleElement) { if (this.getFocusableElements().indexOf(sampleElement) >= 0) return sampleElement; @@ -171,46 +163,42 @@ cr.define('cr.ui', function() { } return this.getCustomEquivalent(sampleElement); - }, + } /** * @param {string=} opt_type An optional type to search for. * @return {?Element} The first focusable element with |type|. */ - getFirstFocusable: function(opt_type) { - var filter = opt_type ? '="' + opt_type + '"' : ''; - var elements = this.root.querySelectorAll('[focus-type' + filter + ']'); - for (var i = 0; i < elements.length; ++i) { - if (cr.ui.FocusRow.isFocusable(elements[i])) - return elements[i]; - } - return null; - }, + getFirstFocusable(opt_type) { + const element = this.getFocusableElements().find( + el => !opt_type || el.getAttribute('focus-type') == opt_type); + return element || null; + } /** @return {!Array<!Element>} Registered, focusable elements. */ - getFocusableElements: function() { + getFocusableElements() { return this.getElements().filter(cr.ui.FocusRow.isFocusable); - }, + } /** * @param {!Element} element An element to determine a focus type for. * @return {string} The focus type for |element| or '' if none. */ - getTypeForElement: function(element) { + getTypeForElement(element) { return element.getAttribute('focus-type') || ''; - }, + } /** @return {boolean} Whether this row is currently active. */ - isActive: function() { + isActive() { return this.root.classList.contains(FocusRow.ACTIVE_CLASS); - }, + } /** * Enables/disables the tabIndex of the focusable elements in the FocusRow. * tabIndex can be set properly. * @param {boolean} active True if tab is allowed for this row. */ - makeActive: function(active) { + makeActive(active) { if (active == this.isActive()) return; @@ -219,35 +207,35 @@ cr.define('cr.ui', function() { }); this.root.classList.toggle(FocusRow.ACTIVE_CLASS, active); - }, + } /** * @param {!Event} e * @private */ - onBlur_: function(e) { + onBlur_(e) { if (!this.boundary_.contains(/** @type {Element} */ (e.relatedTarget))) return; var currentTarget = /** @type {!Element} */ (e.currentTarget); if (this.getFocusableElements().indexOf(currentTarget) >= 0) this.makeActive(false); - }, + } /** * @param {!Event} e * @private */ - onFocus_: function(e) { + onFocus_(e) { if (this.delegate) this.delegate.onFocus(this, e); - }, + } /** * @param {!Event} e A mousedown event. * @private */ - onMousedown_: function(e) { + onMousedown_(e) { // Only accept left mouse clicks. if (e.button) return; @@ -255,13 +243,13 @@ cr.define('cr.ui', function() { // Allow the element under the mouse cursor to be focusable. if (!e.currentTarget.disabled) e.currentTarget.tabIndex = 0; - }, + } /** * @param {!Event} e The keydown event. * @private */ - onKeydown_: function(e) { + onKeydown_(e) { var elements = this.getFocusableElements(); var currentElement = /** @type {!Element} */ (e.currentTarget); var elementIndex = elements.indexOf(currentElement); @@ -289,10 +277,33 @@ cr.define('cr.ui', function() { this.getEquivalentElement(elementToFocus).focus(); e.preventDefault(); } - }, - }; + } + } + + /** @const {string} */ + FocusRow.ACTIVE_CLASS = 'focus-row-active'; + + + /** @interface */ + class FocusRowDelegate { + /** + * Called when a key is pressed while on a FocusRow's item. If true is + * returned, further processing is skipped. + * @param {!cr.ui.FocusRow} row The row that detected a keydown. + * @param {!Event} e + * @return {boolean} Whether the event was handled. + */ + onKeydown(row, e) {} + + /** + * @param {!cr.ui.FocusRow} row + * @param {!Event} e + */ + onFocus(row, e) {} + } return { - FocusRow: FocusRow, + FocusRow, + FocusRowDelegate, }; }); diff --git a/chromium/ui/webui/resources/js/util.js b/chromium/ui/webui/resources/js/util.js index a5099bf9b56..7139d9baede 100644 --- a/chromium/ui/webui/resources/js/util.js +++ b/chromium/ui/webui/resources/js/util.js @@ -33,6 +33,18 @@ function getSVGElement(id) { } /** + * @return {?Element} The currently focused element (including elements that are + * behind a shadow root), or null if nothing is focused. + */ +function getDeepActiveElement() { + var a = document.activeElement; + while (a && a.shadowRoot && a.shadowRoot.activeElement) { + a = a.shadowRoot.activeElement; + } + return a; +} + +/** * Add an accessible message to the page that will be announced to * users who have spoken feedback on, but will be invisible to all * other users. It's removed right away so it doesn't clutter the DOM. diff --git a/chromium/ui/webui/resources/js/webui_resource_test.js b/chromium/ui/webui/resources/js/webui_resource_test.js index ed8d79e401c..ea7089b0bac 100644 --- a/chromium/ui/webui/resources/js/webui_resource_test.js +++ b/chromium/ui/webui/resources/js/webui_resource_test.js @@ -116,9 +116,28 @@ function assertDeepEquals(expected, observed, opt_message) { } /** - * Defines runTests. + * Decorates |window| with runTests() and endTests(). + * + * @param {{ + * runTests: (function(Object=):void|undefined), + * endTests: (function(boolean):void|undefined) + * }} exports */ (function(exports) { + +/** + * Optional setup and teardown hooks that can be defined in a test scope. + * |setUpPage| is invoked once. |setUp|/|tearDown| are invoked before/after each + * test*() declared in the test scope. + * + * @typedef {{ + * setUpPage: (function(): void|undefined), + * setUp: (function(): void|undefined), + * tearDown: (function(): void|undefined), + * }} + */ +var WebUiTestHarness; + /** * Scope containing testXXX functions. * @type {!Object} @@ -126,6 +145,12 @@ function assertDeepEquals(expected, observed, opt_message) { var testScope = {}; /** + * Test harness entrypoints on |testScope|. + * @type {!WebUiTestHarness} + */ +var testHarness = {}; + +/** * List of test cases. * @type {Array<string>} List of function names for tests to run. */ @@ -170,6 +195,7 @@ var runnerStartTime = 0; function runTests(opt_testScope) { runnerStartTime = performance.now(); testScope = opt_testScope || window; + testHarness = /** @type{!WebUiTestHarness} */ (testScope); for (var name in testScope) { // To avoid unnecessary getting properties, test name first. if (/^test/.test(name) && typeof testScope[name] == 'function') @@ -180,11 +206,22 @@ function runTests(opt_testScope) { cleanTestRun = false; } try { - if (testScope.setUpPage) - testScope.setUpPage(); + if (testHarness.setUpPage) + testHarness.setUpPage(); } catch (err) { cleanTestRun = false; } + startTesting(); +} + +/** + * @suppress {missingProperties} + */ +function startTesting() { + if (window.waitUser) { + setTimeout(startTesting, 1000); + return; + } continueTesting(); } @@ -216,9 +253,9 @@ function continueTesting(opt_asyncTestFailure) { var isAsyncTest = testScope[testName].length; var testError = false; try { - if (testScope.setUp) - testScope.setUp(); - pendingTearDown = testScope.tearDown || null; + if (testHarness.setUp) + testHarness.setUp(); + pendingTearDown = testHarness.tearDown || null; testScope[testName](continueTesting); } catch (err) { console.error('Failure in test ' + testName + '\n' + err); @@ -257,6 +294,16 @@ exports.runTests = runTests; exports.endTests = endTests; })(window); +/** + * @type {!function(Object=):void} + */ +window.runTests; + +/** + * @type {!function(boolean):void} + */ +window.endTests; + window.onerror = function() { window.endTests(false); }; diff --git a/chromium/ui/webui/resources/webui_resources.grd b/chromium/ui/webui/resources/webui_resources.grd index a24f4983bf4..a2b2e2d14bc 100644 --- a/chromium/ui/webui/resources/webui_resources.grd +++ b/chromium/ui/webui/resources/webui_resources.grd @@ -200,6 +200,7 @@ without changes to the corresponding grd file. --> file="images/trash.png" type="BINDATA" /> <if expr="not is_android"> + <part file="cr_components/cr_components_images.grdp" /> <part file="cr_elements_images.grdp" /> </if> </includes> |