Skip to content

Commit

Permalink
[password manager] Extract credential field
Browse files Browse the repository at this point in the history
Extract the password manager's credential fields (an input with a copy
button attached to it) into their own polymer element. This allows
reusing the code for the passkey card.

Bug: 1432717
Change-Id: I0510f570554fe3c7dede61499407d1195f15a30d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4568597
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1150779}
  • Loading branch information
nsatragno authored and Chromium LUCI CQ committed May 30, 2023
1 parent 1a36343 commit 3657614
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 53 deletions.
1 change: 1 addition & 0 deletions chrome/browser/resources/password_manager/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ build_webui("build") {
"dialogs/move_passwords_dialog.ts",
"dialogs/password_preview_item.ts",
"credential_note.ts",
"credential_field.ts",
"password_details_card.ts",
"password_details_section.ts",
"password_list_item.ts",
Expand Down
13 changes: 13 additions & 0 deletions chrome/browser/resources/password_manager/credential_field.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<style include="cr-input-style cr-shared-style shared-style"></style>

<cr-input value="[[value]]" id="inputValue" readonly class="input-field"
label="[[label]]" aria-disabled="true">
<cr-icon-button id="copyButton" class="icon-copy-content"
slot="inline-suffix" title="[[copyButtonLabel]]"
on-click="onCopyValueClick_">
</cr-icon-button>
</cr-input>

<cr-toast id="toast" duration="5000">
<span>[[valueCopiedToastLabel]]</span>
</cr-toast>
93 changes: 93 additions & 0 deletions chrome/browser/resources/password_manager/credential_field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {CrToastElement} from '//resources/cr_elements/cr_toast/cr_toast.js';
import {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './credential_field.html.js';
import {PasswordManagerImpl, PasswordViewPageInteractions} from './password_manager_proxy.js';

export interface CredentialFieldElement {
$: {
inputValue: CrInputElement,
copyButton: CrIconButtonElement,
toast: CrToastElement,
};
}

// An element that represents a credential field with a 'copy' button.
export class CredentialFieldElement extends PolymerElement {
static get is() {
return 'credential-field';
}

static get template() {
return getTemplate();
}

static get properties() {
return {
/**
* The label on the actual input element. Required.
*/
label: String,

/**
* The label on the copy button. Required.
*/
copyButtonLabel: String,

/**
* Text that appears on the toast when clicking the copy button.
* Required.
*/
valueCopiedToastLabel: String,

/**
* Field value.
*/
value: String,

/**
* If set, clicking the copy button will record this password view
* interaction.
*/
interactionId: PasswordViewPageInteractions,
};
}

label: string;
copyButtonLabel: string;
valueCopiedToastLabel: string;
value: string;
interactionId: PasswordViewPageInteractions;

override connectedCallback() {
super.connectedCallback();
assert(this.label);
assert(this.copyButtonLabel);
assert(this.valueCopiedToastLabel);
}

private onCopyValueClick_() {
navigator.clipboard.writeText(this.value).catch(() => {});
this.$.toast.show();
PasswordManagerImpl.getInstance().extendAuthValidity();
if (this.interactionId) {
PasswordManagerImpl.getInstance().recordPasswordViewInteraction(
this.interactionId);
}
}
}

declare global {
interface HTMLElementTagNameMap {
'credential-field': CredentialFieldElement;
}
}

customElements.define(CredentialFieldElement.is, CredentialFieldElement);
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@
padding: var(--cr-form-field-bottom-spacing) var(--cr-section-padding);
}

.input-field {
margin-inline-end: 34px;
--cr-input-padding-start: 20px;
--cr-input-min-height: 40px;
--cr-input-error-display: none;
--cr-input-border-radius: 10px;
--cr-icon-button-margin-start: 0;
--cr-icon-button-margin-end: 0;
}

a.site-link {
max-width: 324px;
}
Expand All @@ -58,14 +48,11 @@
<div class="credential-container">
<div class="row-container">
<div class="column-container">
<cr-input id="usernameValue" value="[[password.username]]" readonly
class="input-field" label="$i18n{usernameLabel}"
aria-disabled="true">
<cr-icon-button id="copyUsernameButton" class="icon-copy-content"
slot="inline-suffix" title="$i18n{copyUsername}"
on-click="onCopyUsernameClick_">
</cr-icon-button>
</cr-input>
<credential-field value="[[password.username]]" id="usernameValue"
label="$i18n{usernameLabel}" copy-button-label="$i18n{copyUsername}"
value-copied-toast-label="$i18n{usernameCopiedToClipboard}"
interaction-id="[[usernameCopyInteraction_]]">
</credential-field>
</div>
<div class="column-container">
<div id="domainLabel" class="cr-form-field-label">
Expand Down Expand Up @@ -124,9 +111,6 @@
</template>
</div>
</div>
<cr-toast id="toast" duration="5000">
<span>[[toastMessage_]]</span>
</cr-toast>
<template is="dom-if" if="[[showEditPasswordDialog_]]" restamp>
<edit-password-dialog on-close="onEditPasswordDialogClosed_"
id="editPasswordDialog" credential="{{password}}">
Expand All @@ -137,3 +121,6 @@
id="deletePasswordDialog" duplicated-password="[[password]]">
</multi-store-delete-password-dialog>
</template>
<cr-toast id="toast" duration="5000">
<span>[[toastMessage_]]</span>
</cr-toast>
18 changes: 8 additions & 10 deletions chrome/browser/resources/password_manager/password_details_card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {CredentialFieldElement} from './credential_field.js';
import {CredentialNoteElement} from './credential_note.js';
import {getTemplate} from './password_details_card.html.js';
import {PasswordManagerImpl, PasswordViewPageInteractions} from './password_manager_proxy.js';
Expand All @@ -38,7 +39,6 @@ declare global {
export interface PasswordDetailsCardElement {
$: {
copyPasswordButton: CrIconButtonElement,
copyUsernameButton: CrIconButtonElement,
deleteButton: CrButtonElement,
domainLabel: HTMLElement,
editButton: CrButtonElement,
Expand All @@ -47,7 +47,7 @@ export interface PasswordDetailsCardElement {
showMore: HTMLAnchorElement,
showPasswordButton: CrIconButtonElement,
toast: CrToastElement,
usernameValue: CrInputElement,
usernameValue: CredentialFieldElement,
};
}

Expand All @@ -67,6 +67,12 @@ export class PasswordDetailsCardElement extends PasswordDetailsCardElementBase {
return {
password: Object,
toastMessage_: String,
usernameCopyInteraction_: {
type: PasswordViewPageInteractions,
value() {
return PasswordViewPageInteractions.USERNAME_COPY_BUTTON_CLICKED;
},
},

showEditPasswordDialog_: Boolean,
showDeletePasswordDialog_: Boolean,
Expand Down Expand Up @@ -121,14 +127,6 @@ export class PasswordDetailsCardElement extends PasswordDetailsCardElementBase {
PasswordViewPageInteractions.PASSWORD_SHOW_BUTTON_CLICKED);
}

private onCopyUsernameClick_() {
navigator.clipboard.writeText(this.password.username).catch(() => {});
this.showToast_(this.i18n('usernameCopiedToClipboard'));
this.extendAuthValidity_();
PasswordManagerImpl.getInstance().recordPasswordViewInteraction(
PasswordViewPageInteractions.USERNAME_COPY_BUTTON_CLICKED);
}

private onDeleteClick_() {
PasswordManagerImpl.getInstance().recordPasswordViewInteraction(
PasswordViewPageInteractions.PASSWORD_DELETE_BUTTON_CLICKED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {CrExpandButtonElement} from 'chrome://resources/cr_elements/cr_expand_bu
export {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
export {OpenWindowProxyImpl} from 'chrome://resources/js/open_window_proxy.js';
export {PluralStringProxy, PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
export {CredentialFieldElement} from './credential_field.js';
export {CredentialNoteElement} from './credential_note.js';
export {AddPasswordDialogElement} from './dialogs/add_password_dialog.js';
export {AuthTimedOutDialogElement} from './dialogs/auth_timed_out_dialog.js';
Expand Down
10 changes: 10 additions & 0 deletions chrome/browser/resources/password_manager/shared_style.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,13 @@ cr-input.password-input::part(input) {
font-family: 'Menlo', monospace;
</if>
}

.input-field {
margin-inline-end: 34px;
--cr-input-padding-start: 20px;
--cr-input-min-height: 40px;
--cr-input-error-display: none;
--cr-input-border-radius: 10px;
--cr-icon-button-margin-start: 0;
--cr-icon-button-margin-end: 0;
}
1 change: 1 addition & 0 deletions chrome/test/data/webui/password_manager/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ build_webui_tests("build") {
"checkup_section_test.ts",
"edit_password_dialog_test.ts",
"move_passwords_dialog_test.ts",
"credential_field_test.ts",
"credential_note_test.ts",
"password_details_card_test.ts",
"password_details_section_test.ts",
Expand Down
63 changes: 63 additions & 0 deletions chrome/test/data/webui/password_manager/credential_field_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://password-manager/password_manager.js';

import {CredentialFieldElement, Page, PasswordManagerImpl, PasswordViewPageInteractions, Router} from 'chrome://password-manager/password_manager.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';

import {TestPasswordManagerProxy} from './test_password_manager_proxy.js';

const LABEL = 'Username';
const COPY_BUTTON_LABEL = 'Copy username';
const VALUE_COPIED_TOAST_LABEL = 'Username copied!';
const VALUE = 'nadeshiko@example.com';
const INTERACTION_ID =
PasswordViewPageInteractions.USERNAME_COPY_BUTTON_CLICKED;

async function createCredentialFieldElement(): Promise<CredentialFieldElement> {
const element = document.createElement('credential-field');
element.label = LABEL;
element.copyButtonLabel = COPY_BUTTON_LABEL;
element.valueCopiedToastLabel = VALUE_COPIED_TOAST_LABEL;
element.value = VALUE;
element.interactionId = INTERACTION_ID;
document.body.appendChild(element);
await flushTasks();
return element;
}

suite('CredentialFieldTest', function() {
let passwordManager: TestPasswordManagerProxy;

setup(function() {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
passwordManager = new TestPasswordManagerProxy();
PasswordManagerImpl.setInstance(passwordManager);
Router.getInstance().navigateTo(Page.PASSWORDS);
return flushTasks();
});

test('element sets all the attributes', async function() {
const element = await createCredentialFieldElement();
assertEquals(element.$.inputValue.value, VALUE);
assertEquals(element.$.inputValue.label, LABEL);
assertEquals(element.$.copyButton.title, COPY_BUTTON_LABEL);
});

test('copy value', async function() {
const element = await createCredentialFieldElement();
assertFalse(element.$.toast.open);

element.$.copyButton.click();
await passwordManager.whenCalled('extendAuthValidity');
assertEquals(
INTERACTION_ID,
await passwordManager.whenCalled('recordPasswordViewInteraction'));

assertTrue(element.$.toast.open);
assertEquals(VALUE_COPIED_TOAST_LABEL, element.$.toast.textContent!.trim());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ suite('PasswordDetailsCardTest', function() {
assertEquals('password', card.$.passwordValue.type);
assertTrue(isVisible(card.$.noteValue));
assertEquals(password.note, card.$.noteValue.note);
assertTrue(isVisible(card.$.copyUsernameButton));
assertTrue(isVisible(card.$.showPasswordButton));
assertTrue(isVisible(card.$.copyPasswordButton));
assertTrue(isVisible(card.$.editButton));
Expand All @@ -75,33 +74,12 @@ suite('PasswordDetailsCardTest', function() {
assertEquals(password.federationText, card.$.passwordValue.value);
assertEquals('text', card.$.passwordValue.type);
assertFalse(isVisible(card.$.noteValue));
assertTrue(isVisible(card.$.copyUsernameButton));
assertFalse(isVisible(card.$.showPasswordButton));
assertFalse(isVisible(card.$.copyPasswordButton));
assertFalse(isVisible(card.$.editButton));
assertTrue(isVisible(card.$.deleteButton));
});

test('Copy username', async function() {
const password = createPasswordEntry({url: 'test.com', username: 'vik'});

const card = await createCardElement(password);

assertTrue(isVisible(card.$.copyUsernameButton));
assertFalse(card.$.toast.open);

card.$.copyUsernameButton.click();
await passwordManager.whenCalled('extendAuthValidity');
assertEquals(
PasswordViewPageInteractions.USERNAME_COPY_BUTTON_CLICKED,
await passwordManager.whenCalled('recordPasswordViewInteraction'));

assertTrue(card.$.toast.open);
assertEquals(
loadTimeData.getString('usernameCopiedToClipboard'),
card.$.toast.textContent!.trim());
});

test('Copy password', async function() {
const password = createPasswordEntry(
{id: 1, url: 'test.com', username: 'vik', password: 'password69'});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const PasswordManagerBrowserTest = class extends PolymerTest {
['App', 'password_manager_app_test.js'],
['Checkup', 'checkup_section_test.js'],
['CheckupDetails', 'checkup_details_section_test.js'],
['CredentialField', 'credential_field_test.js'],
['CredentialNote', 'credential_note_test.js'],
['EditPassword', 'edit_password_dialog_test.js'],
['MovePasswordsDialog', 'move_passwords_dialog_test.js'],
Expand Down

0 comments on commit 3657614

Please sign in to comment.