1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-26 09:33:22 +00:00

[PM-12763] Modify Autofill Animation

This commit is contained in:
Cesar Gonzalez
2024-09-27 10:42:21 -05:00
parent 433ae13513
commit 566991ddda
7 changed files with 110 additions and 155 deletions

View File

@@ -1,43 +0,0 @@
@-webkit-keyframes bitwardenfill {
0% {
-webkit-transform: scale(1, 1);
}
50% {
-webkit-transform: scale(1.2, 1.2);
}
100% {
-webkit-transform: scale(1, 1);
}
}
@-moz-keyframes bitwardenfill {
0% {
transform: scale(1, 1);
}
50% {
transform: scale(1.2, 1.2);
}
100% {
transform: scale(1, 1);
}
}
span[data-bwautofill].com-bitwarden-browser-animated-fill {
display: inline-block;
}
.com-bitwarden-browser-animated-fill {
animation: bitwardenfill 200ms ease-in-out 0ms 1;
-webkit-animation: bitwardenfill 200ms ease-in-out 0ms 1;
}
@media (prefers-reduced-motion) {
.com-bitwarden-browser-animated-fill {
animation: none;
-webkit-animation: none;
}
}

View File

@@ -68,6 +68,8 @@ function setMockWindowLocation({
}
describe("InsertAutofillContentService", () => {
let matchesMedia = false;
window.matchMedia = jest.fn(() => mock<MediaQueryList>({ matches: matchesMedia }));
const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall();
const inlineMenuFieldQualificationService = mock<InlineMenuFieldQualificationService>();
const domQueryService = new DomQueryService();
@@ -85,6 +87,7 @@ describe("InsertAutofillContentService", () => {
let fillScript: AutofillScript;
beforeEach(() => {
matchesMedia = false;
document.body.innerHTML = mockLoginForm;
confirmSpy = jest.spyOn(globalThis, "confirm");
windowLocationSpy = jest.spyOn(globalThis, "location", "get");
@@ -765,45 +768,30 @@ describe("InsertAutofillContentService", () => {
});
describe("will not trigger the animation when...", () => {
it("the element is a non-hidden hidden input type", async () => {
it("the user prefers reduced motion", () => {
matchesMedia = true;
const testElement = document.querySelector(
'input[type="password"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.style.setProperty).not.toHaveBeenCalled();
});
it("the element is a non-hidden hidden input type", () => {
document.body.innerHTML = mockLoginForm + '<input type="hidden" />';
const testElement = document.querySelector(
'input[type="hidden"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
await jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
});
it("the element is a non-hidden textarea", () => {
document.body.innerHTML = mockLoginForm + "<textarea></textarea>";
const testElement = document.querySelector("textarea") as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
});
it("the element is a unsupported tag", () => {
document.body.innerHTML = mockLoginForm + '<div id="input-tag"></div>';
const testElement = document.querySelector("#input-tag") as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
expect(testElement.style.setProperty).not.toHaveBeenCalled();
});
it("the element has a `visibility: hidden;` CSS rule applied to it", () => {
@@ -811,14 +799,12 @@ describe("InsertAutofillContentService", () => {
'input[type="password"]',
) as FillableFormFieldElement;
testElement.style.visibility = "hidden";
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
expect(testElement.style.setProperty).not.toHaveBeenCalled();
});
it("the element has a `display: none;` CSS rule applied to it", () => {
@@ -826,14 +812,12 @@ describe("InsertAutofillContentService", () => {
'input[type="password"]',
) as FillableFormFieldElement;
testElement.style.display = "none";
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
expect(testElement.style.setProperty).not.toHaveBeenCalled();
});
it("a parent of the element has an `opacity: 0;` CSS rule applied to it", () => {
@@ -842,14 +826,12 @@ describe("InsertAutofillContentService", () => {
const testElement = document.querySelector(
'input[type="email"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).not.toHaveBeenCalled();
expect(testElement.classList.remove).not.toHaveBeenCalled();
expect(testElement.style.setProperty).not.toHaveBeenCalled();
});
});
@@ -858,24 +840,15 @@ describe("InsertAutofillContentService", () => {
const testElement = document.querySelector(
'input[type="password"]',
) as FillableFormFieldElement;
jest.spyOn(
insertAutofillContentService["domElementVisibilityService"],
"isElementHiddenByCss",
);
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(
insertAutofillContentService["domElementVisibilityService"].isElementHiddenByCss,
).toHaveBeenCalledWith(testElement);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
expect(testElement.style.setProperty).toHaveBeenCalledWith(
"transition",
"background-color 0.2s ease-out, color 0.2s ease-out, border-color 0.2s ease-out",
"important",
);
});
@@ -884,17 +857,15 @@ describe("InsertAutofillContentService", () => {
const testElement = document.querySelector(
'input[type="email"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
expect(testElement.style.setProperty).toHaveBeenCalledWith(
"background-color",
"rgba(232, 240, 255, 1)",
"important",
);
});
@@ -903,17 +874,15 @@ describe("InsertAutofillContentService", () => {
const testElement = document.querySelector(
'input[type="text"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
expect(testElement.style.setProperty).toHaveBeenCalledWith(
"color",
"rgba(14, 55, 129, 1)",
"important",
);
});
@@ -922,69 +891,53 @@ describe("InsertAutofillContentService", () => {
const testElement = document.querySelector(
'input[type="number"]',
) as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
expect(testElement.style.setProperty).toHaveBeenCalledWith(
"border-color",
"rgba(23, 93, 220, 1)",
"important",
);
});
it("the element is a non-hidden tel input", () => {
document.body.innerHTML = mockLoginForm + '<input type="tel" />';
const testElement = document.querySelector('input[type="tel"]') as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
expect(testElement.style.setProperty).toHaveBeenCalledWith(
"outline",
"2px solid rgba(23, 93, 220, 0.7)",
"important",
);
});
it("the element is a non-hidden url input", () => {
document.body.innerHTML = mockLoginForm + '<input type="url" />';
const testElement = document.querySelector('input[type="url"]') as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.style.setProperty).toHaveBeenCalled();
});
it("the element is a non-hidden span", () => {
document.body.innerHTML = mockLoginForm + '<span id="input-tag"></span>';
const testElement = document.querySelector("#input-tag") as FillableFormFieldElement;
jest.spyOn(testElement.classList, "add");
jest.spyOn(testElement.classList, "remove");
jest.spyOn(testElement.style, "setProperty");
insertAutofillContentService["triggerFillAnimationOnElement"](testElement);
jest.advanceTimersByTime(200);
expect(testElement.classList.add).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.classList.remove).toHaveBeenCalledWith(
"com-bitwarden-browser-animated-fill",
);
expect(testElement.style.setProperty).toHaveBeenCalled();
});
});
});

View File

@@ -7,6 +7,7 @@ import {
elementIsInputElement,
elementIsSelectElement,
elementIsTextAreaElement,
setElementStyles,
} from "../utils";
import { InsertAutofillContentService as InsertAutofillContentServiceInterface } from "./abstractions/insert-autofill-content.service";
@@ -14,6 +15,7 @@ import { CollectAutofillContentService } from "./collect-autofill-content.servic
import DomElementVisibilityService from "./dom-element-visibility.service";
class InsertAutofillContentService implements InsertAutofillContentServiceInterface {
private readonly filledElements: WeakMap<HTMLElement, Record<string, any>> = new Map();
private readonly autofillInsertActions: AutofillInsertActions = {
fill_by_opid: ({ opid, value }) => this.handleFillFieldByOpidAction(opid, value),
click_on_opid: ({ opid }) => this.handleClickOnFieldByOpidAction(opid),
@@ -286,18 +288,64 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
* @private
*/
private triggerFillAnimationOnElement(element: FormFieldElement): void {
const skipAnimatingElement =
elementIsFillableFormField(element) &&
!new Set(["email", "text", "password", "number", "tel", "url"]).has(element?.type);
if (this.domElementVisibilityService.isElementHiddenByCss(element) || skipAnimatingElement) {
const prefersReducedMotion = !!globalThis.matchMedia(`(prefers-reduced-motion: reduce)`)
.matches;
if (prefersReducedMotion) {
return;
}
element.classList.add("com-bitwarden-browser-animated-fill");
setTimeout(() => element.classList.remove("com-bitwarden-browser-animated-fill"), 200);
const skipAnimatingElement =
elementIsInputElement(element) &&
!new Set(["email", "text", "password", "number", "tel", "url", "date"]).has(element?.type);
if (skipAnimatingElement || this.domElementVisibilityService.isElementHiddenByCss(element)) {
return;
}
if (!this.filledElements.has(element)) {
this.filledElements.set(element, {
transition: element.style.transition,
backgroundColor: element.style.backgroundColor,
textColor: element.style.color,
borderColor: element.style.borderColor,
outline: element.style.outline,
});
}
setElementStyles(
element,
{
transition:
"background-color 0.2s ease-out, color 0.2s ease-out, border-color 0.2s ease-out",
backgroundColor: "rgba(232, 240, 255, 1)",
color: "rgba(14, 55, 129, 1)",
borderColor: "rgba(23, 93, 220, 1)",
outline: "2px solid rgba(23, 93, 220, 0.7)",
},
true,
);
element.addEventListener(EVENTS.INPUT, this.resetElementStyles, { once: true });
}
/**
* Reset the element styles to their original values upon user modification of the form field.
*
* @param event - The keyboard event that triggered the reset.
*/
private resetElementStyles = (event: KeyboardEvent): void => {
const element = event.target as HTMLElement;
if (this.filledElements.has(element)) {
const { backgroundColor, textColor, borderColor, outline, transition } =
this.filledElements.get(element);
setElementStyles(element, {
backgroundColor: backgroundColor,
color: textColor,
borderColor: borderColor,
outline: outline,
transition: transition,
});
}
};
/**
* Simulates a click event on the element.
* @param {HTMLElement} element

View File

@@ -142,7 +142,7 @@ export function setElementStyles(
for (const styleProperty in styles) {
element.style.setProperty(
styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case
styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), // Convert camelCase to kebab-case
styles[styleProperty],
priority ? "important" : undefined,
);

View File

@@ -24,7 +24,6 @@
},
{
"all_frames": true,
"css": ["content/autofill.css"],
"js": ["content/trigger-autofill-script-injection.js"],
"matches": ["*://*/*", "file:///*"],
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],

View File

@@ -25,7 +25,6 @@
},
{
"all_frames": true,
"css": ["content/autofill.css"],
"js": ["content/trigger-autofill-script-injection.js", "content/misc-utils.js"],
"matches": ["*://*/*", "file:///*"],
"exclude_matches": ["*://*/*.xml*", "file:///*.xml*"],

View File

@@ -189,7 +189,6 @@ const plugins = [
{ from: "./src/_locales", to: "_locales" },
{ from: "./src/images", to: "images" },
{ from: "./src/popup/images", to: "popup/images" },
{ from: "./src/autofill/content/autofill.css", to: "content" },
],
}),
new MiniCssExtractPlugin({