1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 23:45:37 +00:00

initial shortcut modal

This commit is contained in:
Robyn MacCallum
2025-09-25 09:40:02 -04:00
parent e880338704
commit 648981f1f8
7 changed files with 202 additions and 11 deletions

View File

@@ -348,7 +348,9 @@
<small class="help-block" *ngIf="form.value.enableAutotype">
<b>{{ "important" | i18n }}</b>
{{ "enableAutotypeDescriptionTransitionKey" | i18n }}
<b>{{ "editShortcut" | i18n }}</b></small
<span class="settings-link" *ngIf="this.form.value.enableAutotype" (click)="updateAutotypeShortcut()">
{{ "editShortcut" | i18n }}
</span></small
>
</div>
<div class="form-group">

View File

@@ -58,6 +58,7 @@ import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
import { SetPinComponent } from "../../auth/components/set-pin.component";
import { SetAutotypeShortcutComponent } from "../../autofill/components/set-autotype-shortcut.component";
import { SshAgentPromptType } from "../../autofill/models/ssh-agent-setting";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopAutotypeService } from "../../autofill/services/desktop-autotype.service";
@@ -138,6 +139,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
userHasMasterPassword: boolean;
userHasPinSet: boolean;
userHasAutotypeShortcutSet: boolean;
pinEnabled$: Observable<boolean> = of(true);
@@ -283,14 +286,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
// Autotype is for Windows initially
const isWindows = this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop;
if (isWindows) {
this.configService
.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype)
.pipe(takeUntil(this.destroy$))
.subscribe((enabled) => {
this.showEnableAutotype = enabled;
});
}
const windowsDesktopAutotypeFeatureFlag = true;
this.showEnableAutotype = windowsDesktopAutotypeFeatureFlag;
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
@@ -421,12 +418,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
});
if (isWindows) {
if (true) {
this.billingAccountProfileStateService
.hasPremiumFromAnySource$(activeAccount.id)
.pipe(takeUntil(this.destroy$))
.subscribe((hasPremium) => {
if (hasPremium) {
if (true) {
this.form.controls.enableAutotype.enable();
}
});
@@ -899,6 +896,18 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.desktopAutotypeService.setAutotypeEnabledState(this.form.value.enableAutotype);
}
async updateAutotypeShortcut() {
const dialogRef = SetAutotypeShortcutComponent.open(this.dialogService);
if (dialogRef == null) {
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
this.userHasAutotypeShortcutSet = await firstValueFrom(dialogRef.closed);
this.form.controls.pin.setValue(this.userHasAutotypeShortcutSet, { emitEvent: false });
}
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
let vaultTimeoutOptions: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },

View File

@@ -0,0 +1,33 @@
<form [bitSubmit]="submit" [formGroup]="setShortcutForm">
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>
{{ "editAutotypeShortcut" | i18n }}
</div>
<div bitDialogContent>
<p>
{{ "editAutotypeShortcutDescription" | i18n }}
</p>
<bit-form-field>
<bit-label>{{ "typeShortcut" | i18n }}</bit-label>
<input
class="tw-font-mono"
bitInput
type="text"
formControlName="shortcut"
(keydown)="onShortcutKeydown($event)"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
/>
</bit-form-field>
</div>
<ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -0,0 +1,135 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
DialogModule,
DialogRef,
DialogService,
FormFieldModule,
IconButtonModule,
} from "@bitwarden/components";
import { firstValueFrom } from "rxjs";
@Component({
templateUrl: "set-autotype-shortcut.component.html",
imports: [
DialogModule,
CommonModule,
JslibModule,
ButtonModule,
IconButtonModule,
ReactiveFormsModule,
AsyncActionsModule,
FormFieldModule,
],
})
export class SetAutotypeShortcutComponent implements OnInit {
constructor(
private accountService: AccountService,
private dialogRef: DialogRef,
private formBuilder: FormBuilder,
// Autotype service?
) { }
ngOnInit(): void {
// set form value from state
}
setShortcutForm = this.formBuilder.group({
shortcut: [
"",
[Validators.required, this.shortcutCombinationValidator()],
],
requireMasterPasswordOnClientRestart: true,
});
submit = async () => {
const shortcutFormControl = this.setShortcutForm.controls.shortcut;
if (Utils.isNullOrWhitespace(shortcutFormControl.value) || shortcutFormControl.invalid) {
return;
}
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Save shortcut via autotype service
console.log(shortcutFormControl.value);
this.dialogRef.close(true);
};
static open(dialogService: DialogService) {
return dialogService.open<boolean>(SetAutotypeShortcutComponent);
}
onShortcutKeydown(event: KeyboardEvent): void {
event.preventDefault();
const shortcut = this.buildShortcutFromEvent(event);
if (shortcut != null) {
this.setShortcutForm.controls.shortcut.setValue(shortcut);
this.setShortcutForm.controls.shortcut.markAsDirty();
this.setShortcutForm.controls.shortcut.updateValueAndValidity();
}
}
private buildShortcutFromEvent(event: KeyboardEvent): string | null {
const hasCtrl = event.ctrlKey;
const hasAlt = event.altKey;
const hasShift = event.shiftKey;
// Require at least one modifier (Ctrl, Alt, or Shift)
if (!hasCtrl && !hasAlt && !hasShift) {
return null;
}
const key = event.key;
// Ignore pure modifier keys themselves
if (key === "Control" || key === "Alt" || key === "Shift" || key === "Meta") {
return null;
}
// Accept a single alphanumeric letter or number as the base key
const isAlphaNumeric = typeof key === "string" && /^[a-zA-Z0-9]$/.test(key);
if (!isAlphaNumeric) {
return null;
}
const parts: string[] = [];
if (hasCtrl) {
parts.push("Ctrl");
}
if (hasAlt) {
parts.push("Alt");
}
if (hasShift) {
parts.push("Shift");
}
parts.push(key.toUpperCase());
return parts.join("+");
}
private shortcutCombinationValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = (control.value ?? "").toString();
if (value.length === 0) {
return null; // handled by required
}
// Must include at least one modifier and end with a single alphanumeric
// Valid examples: Ctrl+A, Alt+5, Shift+Z, Ctrl+Alt+7, Ctrl+Shift+X, Alt+Shift+Q
const pattern = /^(?=.*\b(Ctrl|Alt|Shift)\b)(?:Ctrl\+)?(?:Alt\+)?(?:Shift\+)?[A-Z0-9]$/i;
return pattern.test(value) ? null : { invalidShortcut: true };
};
}
}

View File

@@ -5,6 +5,7 @@ import { LogService } from "@bitwarden/logging";
import { WindowMain } from "../../main/window.main";
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
export class MainDesktopAutotypeService {
autotypeKeyboardShortcut: AutotypeKeyboardShortcut;

View File

@@ -4086,6 +4086,12 @@
"enableAutotypeDescription": {
"message": "Bitwarden does not validate input locations, be sure you are in the right window and field before using the shortcut."
},
"typeShortcut": {
"message": "Type shortcut"
},
"editAutotypeShortcutDescription": {
"message": "Include one of the following modifiers: Ctrl, Alt, or Shift, and a letter or number."
},
"moreBreadcrumbs": {
"message": "More breadcrumbs",
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."

View File

@@ -360,6 +360,11 @@ form,
}
}
.settings-link {
color: #175ddc;
font-weight: bold;
}
app-root > #loading,
.loading {
display: flex;