mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-30 22:41:33 +01:00
[PM-14954] implement multi input totp styling (#12449)
* update menu and button position for multi-input totp * update test to better handle breaking changes * fix sizing bug by filtering duplicate opid fields * update getTotpFields usage * revert private changes per feedback * Update apps/browser/src/autofill/utils/index.ts with positive fucntion Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com> * fix type and update rectNotZero function --------- Co-authored-by: Evan Bassler <evanbassler@EvanBasslersMBP.attlocal.net> Co-authored-by: Jonathan Prusik <jprusik@users.noreply.github.com> Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net>
This commit is contained in:
parent
83ee64ba1d
commit
d471fe0418
@ -30,6 +30,7 @@
|
||||
"dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:watch:all": "jest --watchAll"
|
||||
"test:watch:all": "jest --watchAll",
|
||||
"test:clearCache": "jest --clear-cache"
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,17 @@ export type InlineMenuElementPosition = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export type FieldRect = {
|
||||
bottom: number;
|
||||
height: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
width: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type InlineMenuPosition = {
|
||||
button?: InlineMenuElementPosition;
|
||||
list?: InlineMenuElementPosition;
|
||||
@ -134,6 +145,7 @@ export type OverlayBackgroundExtensionMessage = {
|
||||
isFieldCurrentlyFilling?: boolean;
|
||||
subFrameData?: SubFrameOffsetData;
|
||||
focusedFieldData?: FocusedFieldData;
|
||||
allFieldsRect?: any;
|
||||
isOpeningFullInlineMenu?: boolean;
|
||||
styles?: Partial<CSSStyleDeclaration>;
|
||||
data?: LockedVaultPendingNotificationsData;
|
||||
|
@ -2913,6 +2913,124 @@ describe("OverlayBackground", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("handles menu position when input is focused", () => {
|
||||
it("sets button and menu width and position when non-multi-input totp field is focused", async () => {
|
||||
const subframe = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
url: "",
|
||||
frameId: 0,
|
||||
};
|
||||
|
||||
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
|
||||
focusedFieldRects: {
|
||||
width: 49.328125,
|
||||
height: 64,
|
||||
top: 302.171875,
|
||||
left: 1270.8125,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe);
|
||||
const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe);
|
||||
|
||||
expect(menuPostion).toEqual({
|
||||
width: "49px",
|
||||
top: "366px",
|
||||
left: "1271px",
|
||||
});
|
||||
expect(buttonPostion).toEqual({
|
||||
width: "34px",
|
||||
height: "34px",
|
||||
top: "317px",
|
||||
left: "1271px",
|
||||
});
|
||||
});
|
||||
it("sets button and menu width and position when multi-input totp field is focused", async () => {
|
||||
const subframe = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
url: "",
|
||||
frameId: 0,
|
||||
};
|
||||
|
||||
const totpFields = [
|
||||
createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__0" }),
|
||||
createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__1" }),
|
||||
createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__2" }),
|
||||
];
|
||||
const allFieldData = [
|
||||
createAutofillFieldMock({
|
||||
autoCompleteType: "one-time-code",
|
||||
opid: "__0",
|
||||
rect: {
|
||||
x: 1041.5,
|
||||
y: 302.171875,
|
||||
width: 49.328125,
|
||||
height: 64,
|
||||
top: 302.171875,
|
||||
right: 1090.828125,
|
||||
bottom: 366.171875,
|
||||
left: 1041.5,
|
||||
},
|
||||
}),
|
||||
createAutofillFieldMock({
|
||||
autoCompleteType: "one-time-code",
|
||||
opid: "__1",
|
||||
rect: {
|
||||
x: 1098.828125,
|
||||
y: 302.171875,
|
||||
width: 49.328125,
|
||||
height: 64,
|
||||
top: 302.171875,
|
||||
right: 1148.15625,
|
||||
bottom: 366.171875,
|
||||
left: 1098.828125,
|
||||
},
|
||||
}),
|
||||
createAutofillFieldMock({
|
||||
autoCompleteType: "one-time-code",
|
||||
opid: "__2",
|
||||
rect: {
|
||||
x: 1156.15625,
|
||||
y: 302.171875,
|
||||
width: 249.328125,
|
||||
height: 64,
|
||||
top: 302.171875,
|
||||
right: 2205.484375,
|
||||
bottom: 366.171875,
|
||||
left: 2156.15625,
|
||||
},
|
||||
}),
|
||||
];
|
||||
overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({
|
||||
focusedFieldRects: {
|
||||
width: 49.328125,
|
||||
height: 64,
|
||||
top: 302.171875,
|
||||
left: 1270.8125,
|
||||
},
|
||||
});
|
||||
|
||||
overlayBackground["allFieldData"] = allFieldData;
|
||||
jest.spyOn(overlayBackground as any, "isTotpFieldForCurrentField").mockReturnValue(true);
|
||||
jest.spyOn(overlayBackground as any, "getTotpFields").mockReturnValue(totpFields);
|
||||
|
||||
const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe);
|
||||
const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe);
|
||||
expect(menuPostion).toEqual({
|
||||
width: "1164px",
|
||||
top: "366px",
|
||||
left: "1042px",
|
||||
});
|
||||
expect(buttonPostion).toEqual({
|
||||
width: "34px",
|
||||
height: "34px",
|
||||
top: "292px",
|
||||
left: "2187px",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("triggerDelayedAutofillInlineMenuClosure message handler", () => {
|
||||
it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => {
|
||||
|
@ -70,6 +70,7 @@ import {
|
||||
generateDomainMatchPatterns,
|
||||
generateRandomChars,
|
||||
isInvalidResponseStatusCode,
|
||||
rectHasSize,
|
||||
specialCharacterToKeyMap,
|
||||
} from "../utils";
|
||||
|
||||
@ -130,6 +131,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
private currentInlineMenuCiphersCount: number = 0;
|
||||
private currentAddNewItemData: CurrentAddNewItemData;
|
||||
private focusedFieldData: FocusedFieldData;
|
||||
private allFieldData: AutofillField[];
|
||||
private isFieldCurrentlyFocused: boolean = false;
|
||||
private isFieldCurrentlyFilling: boolean = false;
|
||||
private isInlineMenuButtonVisible: boolean = false;
|
||||
@ -1367,6 +1369,71 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
this.isInlineMenuListVisible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the totp fields for the tab and frame of the currently focused field
|
||||
*/
|
||||
private getTotpFields(): AutofillField[] {
|
||||
const currentTabId = this.focusedFieldData?.tabId;
|
||||
const currentFrameId = this.focusedFieldData?.frameId;
|
||||
const pageDetailsMap = this.pageDetailsForTab[currentTabId];
|
||||
const pageDetails = pageDetailsMap?.get(currentFrameId);
|
||||
|
||||
const fields = pageDetails.details.fields;
|
||||
const totpFields = fields.filter((f) =>
|
||||
this.inlineMenuFieldQualificationService.isTotpField(f),
|
||||
);
|
||||
|
||||
return totpFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates the postion and width for multi-input totp field inline menu
|
||||
* @param totpFieldArray - the totp fields used to evaluate the position of the menu
|
||||
*/
|
||||
private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) {
|
||||
// Filter the fields based on the provided totpfields
|
||||
const filteredObjects = this.allFieldData.filter((obj) =>
|
||||
totpFieldArray.some((o) => o.opid === obj.opid),
|
||||
);
|
||||
|
||||
// Return null if no matching objects are found
|
||||
if (filteredObjects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// Calculate the smallest left and largest right values to determine width
|
||||
const left = Math.min(
|
||||
...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.left),
|
||||
);
|
||||
const largestRight = Math.max(
|
||||
...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.right),
|
||||
);
|
||||
|
||||
const width = largestRight - left;
|
||||
|
||||
return { left, width };
|
||||
}
|
||||
|
||||
/**
|
||||
* calculates the postion for multi-input totp field inline button
|
||||
* @param totpFieldArray - the totp fields used to evaluate the position of the menu
|
||||
*/
|
||||
private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) {
|
||||
const filteredObjects = this.allFieldData.filter((obj) =>
|
||||
totpFieldArray.some((o) => o.opid === obj.opid),
|
||||
);
|
||||
|
||||
if (filteredObjects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const maxRight = Math.max(...filteredObjects.map((obj) => obj.rect.right));
|
||||
const maxObject = filteredObjects.find((obj) => obj.rect.right === maxRight);
|
||||
const top = maxObject.rect.top - maxObject.rect.height * 0.39;
|
||||
const left = maxRight - maxObject.rect.height * 0.3;
|
||||
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the position of either the inline menu list or button. The position
|
||||
* is based on the focused field's position and dimensions.
|
||||
@ -1472,8 +1539,17 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
const subFrameTopOffset = subFrameOffsets?.top || 0;
|
||||
const subFrameLeftOffset = subFrameOffsets?.left || 0;
|
||||
|
||||
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
|
||||
const { width, height } = this.focusedFieldData.focusedFieldRects;
|
||||
let { top, left } = this.focusedFieldData.focusedFieldRects;
|
||||
const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles;
|
||||
|
||||
if (this.isTotpFieldForCurrentField()) {
|
||||
const totpFields = this.getTotpFields();
|
||||
if (totpFields.length > 1) {
|
||||
({ left, top } = this.calculateTotpMultiInputButtonBounds(totpFields));
|
||||
}
|
||||
}
|
||||
|
||||
let elementOffset = height * 0.37;
|
||||
if (height >= 35) {
|
||||
elementOffset = height >= 50 ? height * 0.47 : height * 0.42;
|
||||
@ -1512,7 +1588,16 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
const subFrameTopOffset = subFrameOffsets?.top || 0;
|
||||
const subFrameLeftOffset = subFrameOffsets?.left || 0;
|
||||
|
||||
const { top, left, width, height } = this.focusedFieldData.focusedFieldRects;
|
||||
const { top, height } = this.focusedFieldData.focusedFieldRects;
|
||||
let { left, width } = this.focusedFieldData.focusedFieldRects;
|
||||
|
||||
if (this.isTotpFieldForCurrentField()) {
|
||||
const totpFields = this.getTotpFields();
|
||||
|
||||
if (totpFields.length > 1) {
|
||||
({ left, width } = this.calculateTotpMultiInputMenuBounds(totpFields));
|
||||
}
|
||||
}
|
||||
|
||||
this.inlineMenuPosition.list = {
|
||||
top: Math.round(top + height + subFrameTopOffset),
|
||||
@ -1535,7 +1620,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
* @param sender - The sender of the extension message
|
||||
*/
|
||||
private setFocusedFieldData(
|
||||
{ focusedFieldData }: OverlayBackgroundExtensionMessage,
|
||||
{ focusedFieldData, allFieldsRect }: OverlayBackgroundExtensionMessage,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
) {
|
||||
if (
|
||||
@ -1552,6 +1637,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
|
||||
const previousFocusedFieldData = this.focusedFieldData;
|
||||
this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId };
|
||||
this.allFieldData = allFieldsRect;
|
||||
this.isFieldCurrentlyFocused = true;
|
||||
|
||||
if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { FieldRect } from "../background/abstractions/overlay.background";
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { AutofillFieldQualifierType } from "../enums/autofill-field.enums";
|
||||
@ -124,4 +125,9 @@ export default class AutofillField {
|
||||
fieldQualifier?: AutofillFieldQualifierType;
|
||||
|
||||
accountCreationFieldType?: InlineMenuAccountCreationFieldTypes;
|
||||
|
||||
/**
|
||||
* used for totp multiline calculations
|
||||
*/
|
||||
fieldRect?: FieldRect;
|
||||
}
|
||||
|
@ -957,8 +957,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ
|
||||
accountCreationFieldType: autofillFieldData?.accountCreationFieldType,
|
||||
};
|
||||
|
||||
const allFields = this.formFieldElements;
|
||||
const allFieldsRect = [];
|
||||
|
||||
for (const key of allFields.keys()) {
|
||||
const rect = await this.getMostRecentlyFocusedFieldRects(key);
|
||||
allFieldsRect.push({ ...allFields.get(key), rect }); // Add the combined result to the array
|
||||
}
|
||||
|
||||
await this.sendExtensionMessage("updateFocusedFieldData", {
|
||||
focusedFieldData: this.focusedFieldData,
|
||||
allFieldsRect,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { FieldRect } from "../background/abstractions/overlay.background";
|
||||
import { AutofillPort } from "../enums/autofill-port.enum";
|
||||
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
|
||||
|
||||
@ -545,6 +546,17 @@ export const specialCharacterToKeyMap: Record<string, string> = {
|
||||
"/": "forwardSlashCharacterDescriptor",
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the current rect values are not all 0.
|
||||
*/
|
||||
export function rectHasSize(rect: FieldRect): boolean {
|
||||
if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all the values corresponding to the specified keys in an object are null.
|
||||
* If no keys are specified, checks all keys in the object.
|
||||
|
Loading…
Reference in New Issue
Block a user