Commit 394e764a authored by jdoerrie's avatar jdoerrie Committed by Commit Bot

Add Show Password Button To Settings Page

This change adds a show password button directly in chrome://settings/passwords
to reduce the number of clicks necessary to view a given password.

Bug: 670986
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: If54c92d2ee6c2e64f90a5280e59a4c59af182740
Reviewed-on: https://chromium-review.googlesource.com/575143
Commit-Queue: Jan Wilken Dörrie <jdoerrie@chromium.org>
Reviewed-by: default avatarHector Carmona <hcarmona@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491454}
parent 2876f93d
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
'target_name': 'password_list_item', 'target_name': 'password_list_item',
'dependencies': [ 'dependencies': [
'../compiled_resources2.gyp:focus_row_behavior', '../compiled_resources2.gyp:focus_row_behavior',
'<(EXTERNS_GYP):passwords_private', 'show_password_behavior',
], ],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'], 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
}, },
...@@ -74,6 +74,13 @@ ...@@ -74,6 +74,13 @@
'target_name': 'password_edit_dialog', 'target_name': 'password_edit_dialog',
'dependencies': [ 'dependencies': [
'<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-input/compiled_resources2.gyp:paper-input-extracted', '<(DEPTH)/third_party/polymer/v1_0/components-chromium/paper-input/compiled_resources2.gyp:paper-input-extracted',
'show_password_behavior',
],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
},
{
'target_name': 'show_password_behavior',
'dependencies': [
'<(EXTERNS_GYP):passwords_private', '<(EXTERNS_GYP):passwords_private',
], ],
'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'], 'includes': ['../../../../../third_party/closure_compiler/compile_js2.gypi'],
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input.html"> <link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input.html">
<link rel="import" href="../icons.html"> <link rel="import" href="../icons.html">
<link rel="import" href="../settings_shared_css.html"> <link rel="import" href="../settings_shared_css.html">
<link rel="import" href="show_password_behavior.html">
<dom-module id="password-edit-dialog"> <dom-module id="password-edit-dialog">
<template> <template>
......
...@@ -13,20 +13,10 @@ ...@@ -13,20 +13,10 @@
Polymer({ Polymer({
is: 'password-edit-dialog', is: 'password-edit-dialog',
properties: { behaviors: [ShowPasswordBehavior],
/**
* The password that is being displayed.
* @type {!chrome.passwordsPrivate.PasswordUiEntry}
*/
item: Object,
/** Holds the plaintext password when requested. */
password: String,
},
/** @override */ /** @override */
attached: function() { attached: function() {
this.password = '';
this.$.dialog.showModal(); this.$.dialog.showModal();
}, },
...@@ -35,61 +25,6 @@ Polymer({ ...@@ -35,61 +25,6 @@ Polymer({
this.$.dialog.close(); this.$.dialog.close();
}, },
/**
* Gets the password input's type. Should be 'text' when password is visible
* or when there's federated text otherwise 'password'.
* @private
*/
getPasswordInputType_: function() {
return this.password || this.item.federationText ? 'text' : 'password';
},
/**
* Gets the title text for the show/hide icon.
* @param {string} password
* @param {string} hide The i18n text to use for 'Hide'
* @param {string} show The i18n text to use for 'Show'
* @private
*/
showPasswordTitle_: function(password, hide, show) {
return password ? hide : show;
},
/**
* Get the right icon to display when hiding/showing a password.
* @return {string}
* @private
*/
getIconClass_: function() {
return this.password ? 'icon-visibility-off' : 'icon-visibility';
},
/**
* Gets the text of the password. Will use the value of |password| unless it
* cannot be shown, in which case it will be spaces. It can also be the
* federated text.
* @private
*/
getPassword_: function() {
if (!this.item)
return '';
return this.item.federationText || this.password ||
' '.repeat(this.item.numCharactersInPassword);
},
/**
* Handler for tapping the show/hide button. Will fire an event to request the
* password for this login pair.
* @private
*/
onShowPasswordButtonTap_: function() {
if (this.password)
this.password = '';
else
this.fire('show-password', this.item.loginPair); // Request the password.
},
/** /**
* Handler for tapping the 'done' button. Should just dismiss the dialog. * Handler for tapping the 'done' button. Should just dismiss the dialog.
* @private * @private
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<link rel="import" href="../focus_row_behavior.html"> <link rel="import" href="../focus_row_behavior.html">
<link rel="import" href="../settings_shared_css.html"> <link rel="import" href="../settings_shared_css.html">
<link rel="import" href="passwords_shared_css.html"> <link rel="import" href="passwords_shared_css.html">
<link rel="import" href="show_password_behavior.html">
<dom-module id="password-list-item"> <dom-module id="password-list-item">
<template> <template>
...@@ -18,6 +19,15 @@ ...@@ -18,6 +19,15 @@
direction: rtl; direction: rtl;
display: flex; display: flex;
} }
#password {
/* Since #password is an input element this is necessary to prevent
* Chrome from using the operating system's font instead of the Material
* Design font.
*/
font-family: inherit;
font-size: inherit;
}
</style> </style>
<div class="list-item" focus-row-container> <div class="list-item" focus-row-container>
<div class="website-column no-min-width" <div class="website-column no-min-width"
...@@ -36,22 +46,31 @@ ...@@ -36,22 +46,31 @@
id="username">[[item.loginPair.username]]</div> id="username">[[item.loginPair.username]]</div>
<div class="password-column"> <div class="password-column">
<template is="dom-if" if="[[!item.federationText]]"> <template is="dom-if" if="[[!item.federationText]]">
<!-- Password type and disabled in order to match mock. -->
<input id="password" aria-label=$i18n{editPasswordPasswordLabel} <input id="password" aria-label=$i18n{editPasswordPasswordLabel}
type="password" class="password-field text-elide" disabled type="[[getPasswordInputType_(item, password)]]"
value="[[getEmptyPassword_(item.numCharactersInPassword)]]"> on-tap="onReadonlyInputTap_" class="password-field" readonly
value="[[getPassword_(item, password)]]">
</input> </input>
<button is="paper-icon-button-light" id="showPasswordButton"
class$="[[getIconClass_(item, password)]]"
on-tap="onShowPasswordButtonTap_"
title="[[showPasswordTitle_(password,
'$i18nPolymer{hidePassword}','$i18nPolymer{showPassword}')]]"
focus-row-control focus-type="showPassword">
</button>
</template> </template>
<template is="dom-if" if="[[item.federationText]]"> <template is="dom-if" if="[[item.federationText]]">
<span class="password-field text-elide">[[item.federationText]]</span> <span class="password-field text-elide" id="federated">
[[item.federationText]]
</span>
</template> </template>
</div>
<button is="paper-icon-button-light" id="passwordMenu" <button is="paper-icon-button-light" id="passwordMenu"
class="icon-more-vert" on-tap="onPasswordMenuTap_" class="icon-more-vert" on-tap="onPasswordMenuTap_"
title="$i18n{moreActions}" focus-row-control title="$i18n{moreActions}" focus-row-control
focus-type="passwordMenu"> focus-type="passwordMenu">
</button> </button>
</div> </div>
</div>
</template> </template>
<script src="password_list_item.js"></script> <script src="password_list_item.js"></script>
</dom-module> </dom-module>
...@@ -10,24 +10,15 @@ ...@@ -10,24 +10,15 @@
Polymer({ Polymer({
is: 'password-list-item', is: 'password-list-item',
behaviors: [FocusRowBehavior], behaviors: [FocusRowBehavior, ShowPasswordBehavior],
properties: {
/** /**
* The password whose info should be displayed. * Selects the password on tap if revealed.
* @type {!chrome.passwordsPrivate.PasswordUiEntry}
*/
item: Array,
},
/**
* Creates an empty password of specified length.
* @param {number} length
* @return {string} password
* @private * @private
*/ */
getEmptyPassword_: function(length) { onReadonlyInputTap_: function() {
return ' '.repeat(length); if (this.password)
this.$$('#password').select();
}, },
/** /**
......
...@@ -18,7 +18,16 @@ ...@@ -18,7 +18,16 @@
<dom-module id="passwords-section"> <dom-module id="passwords-section">
<template> <template>
<style include="settings-shared passwords-shared"></style> <style include="settings-shared passwords-shared">
#savedPasswordsHeading {
/* This adds enough padding so that the labels are aligned with the
* columns. It is necessary due to the absence of the "more actions"
* button in the heading.
*/
-webkit-padding-end: calc(
var(--cr-icon-ripple-size) + var(--cr-icon-button-margin-start));
}
</style>
<div class="settings-box first"> <div class="settings-box first">
<settings-toggle-button id="passwordToggle" class="start primary-toggle" <settings-toggle-button id="passwordToggle" class="start primary-toggle"
aria-label="$i18n{passwords}" no-extension-indicator aria-label="$i18n{passwords}" no-extension-indicator
......
...@@ -276,19 +276,6 @@ Polymer({ ...@@ -276,19 +276,6 @@ Polymer({
this.setPasswordExceptionsListener_)); this.setPasswordExceptionsListener_));
}, },
/**
* Sets the password in the current password dialog if the loginPair matches.
* @param {!chrome.passwordsPrivate.LoginPair} loginPair
* @param {string} password
*/
setPassword: function(loginPair, password) {
if (this.activePassword &&
this.activePassword.loginPair.urls.origin == loginPair.urls.origin &&
this.activePassword.loginPair.username == loginPair.username) {
this.$$('password-edit-dialog').password = password;
}
},
/** /**
* Shows the edit password dialog. * Shows the edit password dialog.
* @param {!Event} e * @param {!Event} e
...@@ -385,8 +372,9 @@ Polymer({ ...@@ -385,8 +372,9 @@ Polymer({
*/ */
showPassword_: function(event) { showPassword_: function(event) {
this.passwordManager_.getPlaintextPassword( this.passwordManager_.getPlaintextPassword(
/** @type {!PasswordManager.LoginPair} */ (event.detail), item => { /** @type {!PasswordManager.LoginPair} */ (event.detail.item.loginPair),
this.setPassword(item.loginPair, item.plaintextPassword); item => {
event.detail.password = item.plaintextPassword;
}); });
}, },
......
...@@ -13,18 +13,18 @@ ...@@ -13,18 +13,18 @@
.website-column { .website-column {
display: flex; display: flex;
flex: 3; flex: 1;
} }
.username-column { .username-column {
flex: 2; flex: 1;
margin: 0 8px; margin: 0 8px;
} }
.password-column { .password-column {
align-items: center; align-items: center;
display: flex; display: flex;
flex: 2; flex: 1;
} }
.password-field { .password-field {
......
<link rel="import" href="chrome://resources/html/polymer.html">
<script src="show_password_behavior.js"></script>
// Copyright 2017 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.
/**
* This behavior bundles functionality required to show a password to the user.
* It is used by both <password-list-item> and <password-edit-dialog>.
*
* @polymerBehavior
*/
var ShowPasswordBehavior = {
properties: {
/**
* The password that is being displayed.
* @type {!chrome.passwordsPrivate.PasswordUiEntry}
*/
item: Object,
/**
* Holds the plaintext password when requested.
* Initializing it to the empty string is necessary to indicate that the
* password hasn't been fetched yet.
*/
password: {
type: String,
value: '',
},
},
/**
* Gets the password input's type. Should be 'text' when password is visible
* or when there's federated text otherwise 'password'.
* @private
*/
getPasswordInputType_: function() {
return this.password || this.item.federationText ? 'text' : 'password';
},
/**
* Gets the title text for the show/hide icon.
* @param {string} password
* @param {string} hide The i18n text to use for 'Hide'
* @param {string} show The i18n text to use for 'Show'
* @private
*/
showPasswordTitle_: function(password, hide, show) {
return password ? hide : show;
},
/**
* Get the right icon to display when hiding/showing a password.
* @return {string}
* @private
*/
getIconClass_: function() {
return this.password ? 'icon-visibility-off' : 'icon-visibility';
},
/**
* Gets the text of the password. Will use the value of |password| unless it
* cannot be shown, in which case it will be spaces. It can also be the
* federated text.
* @private
*/
getPassword_: function() {
if (!this.item)
return '';
return this.item.federationText || this.password ||
' '.repeat(this.item.numCharactersInPassword);
},
/**
* Handler for tapping the show/hide button. Will fire an event to request the
* password for this login pair.
* @private
*/
onShowPasswordButtonTap_: function() {
if (this.password)
this.password = '';
else
this.fire('show-password', this); // Request the password.
},
};
...@@ -714,6 +714,12 @@ ...@@ -714,6 +714,12 @@
<structure name="IDR_SETTINGS_ADDRESS_EDIT_DIALOG_JS" <structure name="IDR_SETTINGS_ADDRESS_EDIT_DIALOG_JS"
file="passwords_and_forms_page/address_edit_dialog.js" file="passwords_and_forms_page/address_edit_dialog.js"
type="chrome_html" /> type="chrome_html" />
<structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_HTML"
file="passwords_and_forms_page/show_password_behavior.html"
type="chrome_html" />
<structure name="IDR_SETTINGS_SHOW_PASSWORD_BEHAVIOR_JS"
file="passwords_and_forms_page/show_password_behavior.js"
type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORD_LIST_ITEM_HTML" <structure name="IDR_SETTINGS_PASSWORD_LIST_ITEM_HTML"
file="passwords_and_forms_page/password_list_item.html" file="passwords_and_forms_page/password_list_item.html"
type="chrome_html" /> type="chrome_html" />
......
...@@ -134,6 +134,20 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() { ...@@ -134,6 +134,20 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() {
return passwordsSection; return passwordsSection;
} }
/**
* Helper method used to create a password list item.
* @param {!chrome.passwordsPrivate.PasswordUiEntry} passwordItem
* @return {!Object}
* @private
*/
function createPasswordListItem(passwordItem) {
var passwordListItem = document.createElement('password-list-item');
passwordListItem.item = passwordItem;
document.body.appendChild(passwordListItem);
Polymer.dom.flush();
return passwordListItem;
}
/** /**
* Helper method used to create a password editing dialog. * Helper method used to create a password editing dialog.
* @param {!chrome.passwordsPrivate.PasswordUiEntry} passwordItem * @param {!chrome.passwordsPrivate.PasswordUiEntry} passwordItem
...@@ -461,7 +475,7 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() { ...@@ -461,7 +475,7 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() {
assertTrue(passwordDialog.$.showPasswordButton.hidden); assertTrue(passwordDialog.$.showPasswordButton.hidden);
}); });
test('showSavedPassword', function() { test('showSavedPasswordEditDialog', function() {
var PASSWORD = 'bAn@n@5'; var PASSWORD = 'bAn@n@5';
var item = FakeDataMaker.passwordEntry('goo.gl', 'bart', PASSWORD.length); var item = FakeDataMaker.passwordEntry('goo.gl', 'bart', PASSWORD.length);
var passwordDialog = createPasswordDialog(item); var passwordDialog = createPasswordDialog(item);
...@@ -469,8 +483,6 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() { ...@@ -469,8 +483,6 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() {
assertFalse(passwordDialog.$.showPasswordButton.hidden); assertFalse(passwordDialog.$.showPasswordButton.hidden);
passwordDialog.password = PASSWORD; passwordDialog.password = PASSWORD;
passwordDialog.showPassword = true;
Polymer.dom.flush(); Polymer.dom.flush();
assertEquals(PASSWORD, assertEquals(PASSWORD,
...@@ -481,19 +493,57 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() { ...@@ -481,19 +493,57 @@ TEST_F('SettingsPasswordSectionBrowserTest', 'uiTests', function() {
assertFalse(passwordDialog.$.showPasswordButton.hidden); assertFalse(passwordDialog.$.showPasswordButton.hidden);
}); });
test('showSavedPasswordListItem', function() {
var PASSWORD = 'bAn@n@5';
var item = FakeDataMaker.passwordEntry('goo.gl', 'bart', PASSWORD.length);
var passwordListItem = createPasswordListItem(item);
passwordListItem.password = PASSWORD;
Polymer.dom.flush();
assertEquals(PASSWORD, passwordListItem.$$('#password').value);
// Password should be visible.
assertEquals('text', passwordListItem.$$('#password').type);
// Hide Password Button should be shown.
assertTrue(passwordListItem.$$('#showPasswordButton')
.classList.contains('icon-visibility-off'));
});
// Test will timeout if event is not received. // Test will timeout if event is not received.
test('onShowSavedPassword', function(done) { test('onShowSavedPasswordEditDialog', function(done) {
var item = FakeDataMaker.passwordEntry('goo.gl', 'bart', 1); var expectedItem = FakeDataMaker.passwordEntry('goo.gl', 'bart', 1);
var passwordDialog = createPasswordDialog(item); var passwordDialog = createPasswordDialog(expectedItem);
passwordDialog.addEventListener('show-password', function(event) { passwordDialog.addEventListener('show-password', function(event) {
assertEquals(item.loginPair.urls.origin, event.detail.urls.origin); var actualItem = event.detail.item;
assertEquals(item.loginPair.username, event.detail.username); assertEquals(
expectedItem.loginPair.urls.origin,
actualItem.loginPair.urls.origin);
assertEquals(
expectedItem.loginPair.username, actualItem.loginPair.username);
done(); done();
}); });
MockInteractions.tap(passwordDialog.$.showPasswordButton); MockInteractions.tap(passwordDialog.$.showPasswordButton);
}); });
test('onShowSavedPasswordListItem', function(done) {
var expectedItem = FakeDataMaker.passwordEntry('goo.gl', 'bart', 1);
var passwordListItem = createPasswordListItem(expectedItem);
passwordListItem.addEventListener('show-password', function(event) {
var actualItem = event.detail.item;
assertEquals(
expectedItem.loginPair.urls.origin,
actualItem.loginPair.urls.origin);
assertEquals(
expectedItem.loginPair.username, actualItem.loginPair.username);
done();
});
MockInteractions.tap(passwordListItem.$$('#showPasswordButton'));
});
}); });
mocha.run(); mocha.run();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment