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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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*"],
|
||||
|
||||
@@ -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*"],
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user