Merge pull request #468 from padloc/feature/unmask-field-on-hover

Various UX improvements for Item View
This commit is contained in:
Martin Kleinschrodt 2022-06-09 14:10:52 +02:00 committed by GitHub
commit 609edd22c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 10 deletions

View File

@ -15,6 +15,7 @@ import { generatePassphrase } from "@padloc/core/src/diceware";
import { randomString, charSets } from "@padloc/core/src/util";
import { app } from "../globals";
import { descriptionForAudit, iconForAudit, titleTextForAudit } from "../lib/audit";
import "./popover";
@customElement("pl-field")
export class FieldElement extends LitElement {
@ -57,10 +58,19 @@ export class FieldElement extends LitElement {
private get _fieldActions() {
const actions = [
...(this._fieldDef.actions || []),
{ icon: "copy", label: $l("Copy"), action: () => this.dispatchEvent(new CustomEvent("copy-clipboard")) },
{
icon: "edit",
label: $l("Edit"),
action: () => {
this.dispatchEvent(new CustomEvent("edit"));
this._drawer.collapsed = true;
},
},
];
if (this._fieldDef.mask) {
if (this._fieldDef.mask && !app.settings.unmaskFieldsOnHover) {
actions.unshift({
icon: this._masked ? "show" : "hide",
label: this._masked ? "show" : "hide",
@ -121,10 +131,16 @@ export class FieldElement extends LitElement {
}
protected _mouseenter() {
if (app.settings.unmaskFieldsOnHover) {
this._masked = false;
}
this._drawer.collapsed = this.editing;
}
protected _mouseleave() {
if (app.settings.unmaskFieldsOnHover) {
this._masked = true;
}
this._drawer.collapsed = true;
}
@ -261,6 +277,8 @@ export class FieldElement extends LitElement {
.placeholder=${$l("Enter Secret")}
type="text"
@input=${() => (this.field.value = this._valueInput.value)}
@focus=${this._expandSuggestions}
@blur=${this._collapseSuggestions}
.value=${this.field.value}
>
<pl-button
@ -270,6 +288,21 @@ export class FieldElement extends LitElement {
>
<pl-icon icon="qrcode"></pl-icon>
</pl-button>
${!this.field.value
? html`
<pl-drawer slot="below" collapsed>
<div class="horizontal layout">
<pl-button
class="tiny skinny transparent"
@click=${() => this.dispatchEvent(new CustomEvent("get-totp-qr"))}
>
<pl-icon icon="qrcode" class="right-margined"></pl-icon>
${$l("Scan QR Code")}
</pl-button>
</div>
</pl-drawer>
`
: ""}
</pl-input>
`;
case "password":
@ -457,12 +490,18 @@ export class FieldElement extends LitElement {
</div>
<pl-drawer class="drawer" collapsed>
<div class="end-justifying spacing horizontal layout">
<div class="end-justifying horizontal wrapping layout">
${this._fieldActions.map(
({ icon, action, label }) => html`
<pl-button class="ghost small slim" @click=${action} style="min-width: 7em">
<pl-icon icon=${icon} class="right-margined"></pl-icon>
<div>${label}</div>
<pl-button
class="transparent small slim"
@click=${() => action(this.field.value)}
style="min-width: 7em"
>
<div class="half-spacing center-alinging horizontal layout">
<pl-icon icon=${icon}></pl-icon>
<div>${label}</div>
</div>
</pl-button>
`
)}

View File

@ -123,7 +123,11 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
return;
}
this._editing = true;
setTimeout(() => this._nameInput && this._nameInput.focus(), 500);
setTimeout(() => {
if (!this.shadowRoot?.activeElement) {
this._nameInput?.focus();
}
}, 300);
} else {
this._editing = false;
}
@ -183,6 +187,12 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
setClipboard(await field.transform(), `${item.name} / ${field.name}`);
}
private async _editField(index: number) {
this.edit();
await this.updateComplete;
setTimeout(() => this._fieldInputs[index]?.focus(), 100);
}
static styles = [
shared,
css`
@ -412,6 +422,7 @@ export class ItemView extends Routing(StateMixin(LitElement)) {
@drop=${(e: DragEvent) => this._drop(e)}
@moveup=${() => this._moveField(index, "up")}
@movedown=${() => this._moveField(index, "down")}
@edit=${() => this._editField(index)}
>
</pl-field>
`

View File

@ -14,7 +14,7 @@ import "./rich-content";
@customElement("pl-report")
export class Audit extends StateMixin(Routing(View)) {
readonly routePattern = /^audit/;
readonly routePattern = /^report/;
@state()
private _selected: Vault | Org | null = null;
@ -224,7 +224,7 @@ export class Audit extends StateMixin(Routing(View)) {
`}
<pl-button
class="slim margined transparent"
@click=${() => this.go("items", { audit: type })}
@click=${() => this.go("items", { report: type })}
?hidden=${listItems.length < 6}
>
<div>${$l("Show All")}</div>

View File

@ -22,6 +22,9 @@ export class SettingsDisplay extends StateMixin(LitElement) {
@query("#faviconsButton")
private _faviconsButton: ToggleButton;
@query("#unmaskFieldsOnHoverButton")
private _unmaskFieldsOnHoverButton: ToggleButton;
connectedCallback() {
super.connectedCallback();
this.addEventListener("change", () => this._updateSettings());
@ -31,6 +34,7 @@ export class SettingsDisplay extends StateMixin(LitElement) {
this.app.setSettings({
theme: this._themeSelect.value || undefined,
favicons: this._faviconsButton.active,
unmaskFieldsOnHover: this._unmaskFieldsOnHoverButton.active,
});
}
@ -80,6 +84,31 @@ export class SettingsDisplay extends StateMixin(LitElement) {
</pl-toggle-button>
</div>
</div>
<div class="double-margined box">
<h2 class="padded bg-dark border-bottom semibold horizontal center-aligning layout">
<div class="uppercase stretch">${$l("Masked Fields")}</div>
<pl-icon icon="info-round" class="subtle"></pl-icon>
<pl-popover trigger="hover" class="small double-padded regular" style="max-width: 20em">
${$l(
"If this option is enabled, masked fields such as passwords or credit card " +
"numbers will be unmasked when you move your mouse over them." +
"Disable this option if you would rather use an explicit button."
)}
</pl-popover>
</h2>
<div>
<pl-toggle-button
class="transparent"
id="unmaskFieldsOnHoverButton"
.active=${this.state.settings.unmaskFieldsOnHover}
.label=${$l("Reveal On Hover")}
reverse
>
</pl-toggle-button>
</div>
</div>
</pl-scroller>
</div>
`;

View File

@ -167,9 +167,8 @@ export const misc = css`
}
.list-item:focus:not([aria-selected="true"]) {
background: var(--list-item-focus-background);
color: var(--list-item-focus-color);
/* box-shadow: inset 0.2em 0 0 0 var(--color-highlight); */
box-shadow: inset 0.2em 0 0 0 var(--color-highlight);
}
.section-header {

View File

@ -63,6 +63,8 @@ export class Settings extends Serializable {
securityReportReused = true;
/** Enable checking for compromised passwords */
securityReportCompromised = true;
/** Unmask Fields on hover */
unmaskFieldsOnHover = true;
}
export interface HashedItem {

View File

@ -4,6 +4,7 @@ import { totp } from "./otp";
import { uuid } from "./util";
import { AccountID } from "./account";
import { AttachmentInfo } from "./attachment";
import { openExternalUrl } from "./platform";
/** A tag that can be assigned to a [[VaultItem]] */
export type Tag = string;
@ -48,6 +49,7 @@ export interface FieldDef {
format?: (value: string, masked: boolean) => string;
/** for values that need to be prepared before being copied / filled */
transform?: (value: string) => Promise<string>;
actions?: { icon: string; label: string; action: (value: string) => void }[];
}
/** Available field types and respective meta data (order matters for pattern matching) */
@ -98,6 +100,13 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
get name() {
return $l("URL");
},
actions: [
{
icon: "web",
label: $l("Open"),
action: (value: string) => openExternalUrl(value.startsWith("http") ? value : `https://${value}`),
},
],
},
[FieldType.Date]: {
type: FieldType.Date,