1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 09:43:23 +00:00

[PM-5189] Reworking project structure to ensure we can better differentiate the inline menu feature from other features

This commit is contained in:
Cesar Gonzalez
2024-05-03 14:30:00 -05:00
parent 4038939f46
commit c75004bbd2
11 changed files with 219 additions and 1467 deletions

View File

@@ -9,13 +9,3 @@ exports[`AutofillInlineMenuIframeService initMenuIframe sets up the iframe's att
title="title" title="title"
/> />
`; `;
exports[`AutofillOverlayIframeService initMenuIframe sets up the iframe's attributes 1`] = `
<iframe
allowtransparency="true"
src="chrome-extension://id/overlay/menu.html"
style="all: initial !important; position: fixed !important; display: block !important; z-index: 2147483647 !important; line-height: 0 !important; overflow: hidden !important; transition: opacity 125ms ease-out 0s !important; visibility: visible !important; clip-path: none !important; pointer-events: auto !important; margin: 0px !important; padding: 0px !important; color-scheme: normal !important; opacity: 0 !important; height: 0px;"
tabindex="-1"
title="title"
/>
`;

View File

@@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AutofillOverlayIframeService initMenuIframe sets up the iframe's attributes 1`] = `
<iframe
allowtransparency="true"
src="chrome-extension://id/overlay/menu.html"
style="all: initial !important; position: fixed !important; display: block !important; z-index: 2147483647 !important; line-height: 0 !important; overflow: hidden !important; transition: opacity 125ms ease-out 0s !important; visibility: visible !important; clip-path: none !important; pointer-events: auto !important; margin: 0px !important; padding: 0px !important; color-scheme: normal !important; opacity: 0 !important; height: 0px;"
tabindex="-1"
title="title"
/>
`;

View File

@@ -81,85 +81,3 @@ exports[`AutofillInlineMenuButton initAutofillInlineMenuButton creates the butto
</svg> </svg>
</button> </button>
`; `;
exports[`AutofillOverlayButton initAutofillInlineMenuButton creates the button element with the locked icon when the user's auth status is not Unlocked 1`] = `
<button
aria-label="toggleBitwardenVaultOverlay"
class="inline-menu-button"
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="inline-menu-button-svg-icon logo-locked-icon"
fill="none"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
fill="#175DDC"
/>
<path
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
fill="#fff"
/>
<circle
cx="12.889"
cy="12.889"
fill="#F8F9FA"
r="4.889"
/>
<path
d="M11.26 11.717h2.37v-.848c0-.313-.116-.58-.348-.8a1.17 1.17 0 0 0-.838-.332c-.327 0-.606.11-.838.332a1.066 1.066 0 0 0-.347.8v.848Zm3.851.424v2.546a.4.4 0 0 1-.13.3.44.44 0 0 1-.314.124h-4.445a.44.44 0 0 1-.315-.124.4.4 0 0 1-.13-.3V12.14a.4.4 0 0 1 .13-.3.44.44 0 0 1 .315-.124h.148v-.848c0-.542.204-1.008.612-1.397a2.044 2.044 0 0 1 1.462-.583c.568 0 1.056.194 1.463.583.408.39.611.855.611 1.397v.848h.149a.44.44 0 0 1 .315.124.4.4 0 0 1 .13.3Z"
fill="#555"
/>
</g>
<defs>
<clippath
id="a"
>
<rect
fill="#fff"
height="16"
rx="2"
width="16"
/>
</clippath>
</defs>
</svg>
</button>
`;
exports[`AutofillOverlayButton initAutofillInlineMenuButton creates the button element with the normal icon when the user's auth status is Unlocked 1`] = `
<button
aria-label="toggleBitwardenVaultOverlay"
class="inline-menu-button"
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="inline-menu-button-svg-icon logo-icon"
fill="none"
height="14"
viewBox="0 0 14 14"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
fill="#175DDC"
/>
<path
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
fill="#fff"
/>
</svg>
</button>
`;

View File

@@ -1,83 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AutofillOverlayButton initAutofillOverlayButton creates the button element with the locked icon when the user's auth status is not Unlocked 1`] = `
<button
aria-label="toggleBitwardenVaultOverlay"
class="inline-menu-button"
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="inline-menu-button-svg-icon logo-locked-icon"
fill="none"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
fill="#175DDC"
/>
<path
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
fill="#fff"
/>
<circle
cx="12.889"
cy="12.889"
fill="#F8F9FA"
r="4.889"
/>
<path
d="M11.26 11.717h2.37v-.848c0-.313-.116-.58-.348-.8a1.17 1.17 0 0 0-.838-.332c-.327 0-.606.11-.838.332a1.066 1.066 0 0 0-.347.8v.848Zm3.851.424v2.546a.4.4 0 0 1-.13.3.44.44 0 0 1-.314.124h-4.445a.44.44 0 0 1-.315-.124.4.4 0 0 1-.13-.3V12.14a.4.4 0 0 1 .13-.3.44.44 0 0 1 .315-.124h.148v-.848c0-.542.204-1.008.612-1.397a2.044 2.044 0 0 1 1.462-.583c.568 0 1.056.194 1.463.583.408.39.611.855.611 1.397v.848h.149a.44.44 0 0 1 .315.124.4.4 0 0 1 .13.3Z"
fill="#555"
/>
</g>
<defs>
<clippath
id="a"
>
<rect
fill="#fff"
height="16"
rx="2"
width="16"
/>
</clippath>
</defs>
</svg>
</button>
`;
exports[`AutofillOverlayButton initAutofillOverlayButton creates the button element with the normal icon when the user's auth status is Unlocked 1`] = `
<button
aria-label="toggleBitwardenVaultOverlay"
class="inline-menu-button"
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="inline-menu-button-svg-icon logo-icon"
fill="none"
height="14"
viewBox="0 0 14 14"
width="14"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.66.175A.566.566 0 0 0 12.25 0H1.75a.559.559 0 0 0-.409.175.561.561 0 0 0-.175.41v7c.002.532.105 1.06.305 1.554.189.488.444.948.756 1.368.322.42.682.81 1.076 1.163.365.335.75.649 1.152.939.35.248.718.483 1.103.706.385.222.656.372.815.45.16.08.29.141.386.182A.53.53 0 0 0 7 14a.509.509 0 0 0 .238-.055c.098-.043.225-.104.387-.182.162-.079.438-.23.816-.45.378-.222.75-.459 1.102-.707.403-.29.788-.604 1.154-.939a8.435 8.435 0 0 0 1.076-1.163c.312-.42.567-.88.757-1.367a4.19 4.19 0 0 0 .304-1.555v-7a.55.55 0 0 0-.174-.407Z"
fill="#175DDC"
/>
<path
d="M7 12.365s4.306-2.18 4.306-4.717V1.5H7v10.865Z"
fill="#fff"
/>
</svg>
</button>
`;

View File

@@ -41,7 +41,7 @@ class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
} }
/** /**
* Initializes the overlay button. Facilitates ensuring that the page * Initializes the inline menu button. Facilitates ensuring that the page
* is set up with the expected styles and translations. * is set up with the expected styles and translations.
* *
* @param authStatus - The authentication status of the user * @param authStatus - The authentication status of the user
@@ -114,7 +114,7 @@ class AutofillInlineMenuButton extends AutofillInlineMenuPageElement {
/** /**
* Checks if the button is focused. If it is not, then it posts a message * Checks if the button is focused. If it is not, then it posts a message
* to the parent window indicating that the overlay should be closed. * to the parent window indicating that the inline menu should be closed.
*/ */
private checkButtonFocused() { private checkButtonFocused() {
if (globalThis.document.hasFocus()) { if (globalThis.document.hasFocus()) {

View File

@@ -1,15 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = ` exports[`AutofillInlineMenuList initAutofillInlineMenuList the inline menu with an empty list of ciphers creates the views for the no results inline menu 1`] = `
<div <div
class="overlay-list-container theme_light" class="inline-menu-list-container theme_light"
>
<div
class="no-items inline-menu-list-message"
>
noItemsToShow
</div>
<div
class="inline-menu-list-button-container"
>
<button
aria-label="addNewVaultItem, opensInANewWindow"
class="add-new-item-button inline-menu-list-button"
id="new-item-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .49h16v16H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
newItem
</button>
</div>
</div>
`;
exports[`AutofillInlineMenuList initAutofillInlineMenuList the list of ciphers for an authenticated user creates the view for a list of ciphers 1`] = `
<div
class="inline-menu-list-container theme_light"
> >
<ul <ul
class="overlay-actions-list" class="inline-menu-list-actions"
role="list" role="list"
> >
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -79,7 +130,7 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
</li> </li>
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -148,7 +199,7 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
</li> </li>
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -204,7 +255,7 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
</li> </li>
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -289,7 +340,7 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
</li> </li>
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -359,7 +410,7 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
</li> </li>
<li <li
class="overlay-actions-list-item" class="inline-menu-list-actions-item"
role="listitem" role="listitem"
> >
<div <div
@@ -432,22 +483,22 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the list of ciphers for
</div> </div>
`; `;
exports[`AutofillOverlayList initAutofillInlineMenuList the locked overlay for an unauthenticated user creates the views for the locked overlay 1`] = ` exports[`AutofillInlineMenuList initAutofillInlineMenuList the locked inline menu for an unauthenticated user creates the views for the locked inline menu 1`] = `
<div <div
class="overlay-list-container theme_light" class="inline-menu-list-container theme_light"
> >
<div <div
class="locked-overlay overlay-list-message" class="locked-inline-menu inline-menu-list-message"
id="locked-overlay-description" id="locked-inline-menu-description"
> >
unlockYourAccount unlockYourAccount
</div> </div>
<div <div
class="overlay-list-button-container" class="inline-menu-list-button-container"
> >
<button <button
aria-label="unlockAccount, opensInANewWindow" aria-label="unlockAccount, opensInANewWindow"
class="unlock-button overlay-list-button" class="unlock-button inline-menu-list-button"
id="unlock-button" id="unlock-button"
tabindex="-1" tabindex="-1"
> >
@@ -483,54 +534,3 @@ exports[`AutofillOverlayList initAutofillInlineMenuList the locked overlay for a
</div> </div>
</div> </div>
`; `;
exports[`AutofillOverlayList initAutofillInlineMenuList the overlay with an empty list of ciphers creates the views for the no results overlay 1`] = `
<div
class="overlay-list-container theme_light"
>
<div
class="no-items overlay-list-message"
>
noItemsToShow
</div>
<div
class="overlay-list-button-container"
>
<button
aria-label="addNewVaultItem, opensInANewWindow"
class="add-new-item-button overlay-list-button"
id="new-item-button"
tabindex="-1"
>
<svg
aria-hidden="true"
fill="none"
height="17"
viewBox="0 0 16 17"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M15.222 7.914H8.963a.471.471 0 0 1-.34-.147.512.512 0 0 1-.142-.353V.99c0-.133-.05-.26-.14-.354a.471.471 0 0 0-.68 0 .51.51 0 0 0-.142.354v6.424c0 .132-.051.26-.142.353a.474.474 0 0 1-.34.147H.777a.47.47 0 0 0-.34.146.5.5 0 0 0-.14.354.522.522 0 0 0 .14.353.48.48 0 0 0 .34.147h6.26c.128 0 .25.052.34.146.09.094.142.221.142.354v6.576c0 .132.05.26.14.353a.471.471 0 0 0 .68 0 .512.512 0 0 0 .142-.353V9.414c0-.133.051-.26.142-.354a.474.474 0 0 1 .34-.146h6.26c.127 0 .25-.053.34-.147a.511.511 0 0 0 0-.707.472.472 0 0 0-.34-.146Z"
fill="#175DDC"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 .49h16v16H0z"
fill="#fff"
/>
</clippath>
</defs>
</svg>
newItem
</button>
</div>
</div>
`;

View File

@@ -2,12 +2,12 @@ import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { createInitAutofillOverlayListMessageMock } from "../../../../spec/autofill-mocks"; import { createInitAutofillInlineMenuListMessageMock } from "../../../../spec/autofill-mocks";
import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils"; import { flushPromises, postWindowMessage } from "../../../../spec/testing-utils";
import { AutofillInlineMenuList } from "./autofill-inline-menu-list"; import { AutofillInlineMenuList } from "./autofill-inline-menu-list";
describe("AutofillOverlayList", () => { describe("AutofillInlineMenuList", () => {
globalThis.customElements.define("autofill-inline-menu-list", AutofillInlineMenuList); globalThis.customElements.define("autofill-inline-menu-list", AutofillInlineMenuList);
global.ResizeObserver = jest.fn().mockImplementation(() => ({ global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(), observe: jest.fn(),
@@ -15,12 +15,12 @@ describe("AutofillOverlayList", () => {
disconnect: jest.fn(), disconnect: jest.fn(),
})); }));
let autofillOverlayList: AutofillInlineMenuList; let autofillInlineMenuList: AutofillInlineMenuList;
const portKey: string = "overlayListPortKey"; const portKey: string = "inlineMenuListPortKey";
beforeEach(() => { beforeEach(() => {
document.body.innerHTML = `<autofill-inline-menu-list></autofill-inline-menu-list>`; document.body.innerHTML = `<autofill-inline-menu-list></autofill-inline-menu-list>`;
autofillOverlayList = document.querySelector("autofill-inline-menu-list"); autofillInlineMenuList = document.querySelector("autofill-inline-menu-list");
jest.spyOn(globalThis.document, "createElement"); jest.spyOn(globalThis.document, "createElement");
jest.spyOn(globalThis.parent, "postMessage"); jest.spyOn(globalThis.parent, "postMessage");
}); });
@@ -30,10 +30,10 @@ describe("AutofillOverlayList", () => {
}); });
describe("initAutofillInlineMenuList", () => { describe("initAutofillInlineMenuList", () => {
describe("the locked overlay for an unauthenticated user", () => { describe("the locked inline menu for an unauthenticated user", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage( postWindowMessage(
createInitAutofillOverlayListMessageMock({ createInitAutofillInlineMenuListMessageMock({
authStatus: AuthenticationStatus.Locked, authStatus: AuthenticationStatus.Locked,
cipherList: [], cipherList: [],
portKey, portKey,
@@ -41,13 +41,13 @@ describe("AutofillOverlayList", () => {
); );
}); });
it("creates the views for the locked overlay", () => { it("creates the views for the locked inline menu", () => {
expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot(); expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
}); });
it("allows the user to unlock the vault", () => { it("allows the user to unlock the vault", () => {
const unlockButton = const unlockButton =
autofillOverlayList["overlayListContainer"].querySelector("#unlock-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
unlockButton.dispatchEvent(new Event("click")); unlockButton.dispatchEvent(new Event("click"));
@@ -58,10 +58,10 @@ describe("AutofillOverlayList", () => {
}); });
}); });
describe("the overlay with an empty list of ciphers", () => { describe("the inline menu with an empty list of ciphers", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage( postWindowMessage(
createInitAutofillOverlayListMessageMock({ createInitAutofillInlineMenuListMessageMock({
authStatus: AuthenticationStatus.Unlocked, authStatus: AuthenticationStatus.Unlocked,
ciphers: [], ciphers: [],
portKey, portKey,
@@ -69,13 +69,13 @@ describe("AutofillOverlayList", () => {
); );
}); });
it("creates the views for the no results overlay", () => { it("creates the views for the no results inline menu", () => {
expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot(); expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
}); });
it("allows the user to add a vault item", () => { it("allows the user to add a vault item", () => {
const addVaultItemButton = const addVaultItemButton =
autofillOverlayList["overlayListContainer"].querySelector("#new-item-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
addVaultItemButton.dispatchEvent(new Event("click")); addVaultItemButton.dispatchEvent(new Event("click"));
@@ -88,23 +88,23 @@ describe("AutofillOverlayList", () => {
describe("the list of ciphers for an authenticated user", () => { describe("the list of ciphers for an authenticated user", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock()); postWindowMessage(createInitAutofillInlineMenuListMessageMock());
}); });
it("creates the view for a list of ciphers", () => { it("creates the view for a list of ciphers", () => {
expect(autofillOverlayList["overlayListContainer"]).toMatchSnapshot(); expect(autofillInlineMenuList["inlineMenuListContainer"]).toMatchSnapshot();
}); });
it("loads ciphers on scroll one page at a time", () => { it("loads ciphers on scroll one page at a time", () => {
jest.useFakeTimers(); jest.useFakeTimers();
const originalListOfElements = const originalListOfElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(".cipher-container");
window.dispatchEvent(new Event("scroll")); window.dispatchEvent(new Event("scroll"));
jest.runAllTimers(); jest.runAllTimers();
const updatedListOfElements = const updatedListOfElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(".cipher-container");
expect(originalListOfElements.length).toBe(6); expect(originalListOfElements.length).toBe(6);
expect(updatedListOfElements.length).toBe(8); expect(updatedListOfElements.length).toBe(8);
@@ -112,9 +112,9 @@ describe("AutofillOverlayList", () => {
it("debounces the ciphers scroll handler", () => { it("debounces the ciphers scroll handler", () => {
jest.useFakeTimers(); jest.useFakeTimers();
autofillOverlayList["cipherListScrollDebounceTimeout"] = setTimeout(jest.fn, 0); autofillInlineMenuList["cipherListScrollDebounceTimeout"] = setTimeout(jest.fn, 0);
const handleDebouncedScrollEventSpy = jest.spyOn( const handleDebouncedScrollEventSpy = jest.spyOn(
autofillOverlayList as any, autofillInlineMenuList as any,
"handleDebouncedScrollEvent", "handleDebouncedScrollEvent",
); );
@@ -130,12 +130,12 @@ describe("AutofillOverlayList", () => {
describe("fill cipher button event listeners", () => { describe("fill cipher button event listeners", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
}); });
it("allows the user to fill a cipher on click", () => { it("allows the user to fill a cipher on click", () => {
const fillCipherButton = const fillCipherButton =
autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
fillCipherButton.dispatchEvent(new Event("click")); fillCipherButton.dispatchEvent(new Event("click"));
@@ -147,7 +147,9 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard focus to the next cipher element on ArrowDown", () => { it("allows the user to move keyboard focus to the next cipher element on ArrowDown", () => {
const fillCipherElements = const fillCipherElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const firstFillCipherElement = fillCipherElements[0]; const firstFillCipherElement = fillCipherElements[0];
const secondFillCipherElement = fillCipherElements[1]; const secondFillCipherElement = fillCipherElements[1];
jest.spyOn(secondFillCipherElement as HTMLElement, "focus"); jest.spyOn(secondFillCipherElement as HTMLElement, "focus");
@@ -159,7 +161,9 @@ describe("AutofillOverlayList", () => {
it("directs focus to the first item in the cipher list if no cipher is present after the current one when pressing ArrowDown", () => { it("directs focus to the first item in the cipher list if no cipher is present after the current one when pressing ArrowDown", () => {
const fillCipherElements = const fillCipherElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1]; const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1];
const firstFillCipherElement = fillCipherElements[0]; const firstFillCipherElement = fillCipherElements[0];
jest.spyOn(firstFillCipherElement as HTMLElement, "focus"); jest.spyOn(firstFillCipherElement as HTMLElement, "focus");
@@ -171,7 +175,9 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => { it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => {
const fillCipherElements = const fillCipherElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const firstFillCipherElement = fillCipherElements[0]; const firstFillCipherElement = fillCipherElements[0];
const secondFillCipherElement = fillCipherElements[1]; const secondFillCipherElement = fillCipherElements[1];
jest.spyOn(firstFillCipherElement as HTMLElement, "focus"); jest.spyOn(firstFillCipherElement as HTMLElement, "focus");
@@ -183,7 +189,9 @@ describe("AutofillOverlayList", () => {
it("directs focus to the last item in the cipher list if no cipher is present before the current one when pressing ArrowUp", () => { it("directs focus to the last item in the cipher list if no cipher is present before the current one when pressing ArrowUp", () => {
const fillCipherElements = const fillCipherElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(
".fill-cipher-button",
);
const firstFillCipherElement = fillCipherElements[0]; const firstFillCipherElement = fillCipherElements[0];
const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1]; const lastFillCipherElement = fillCipherElements[fillCipherElements.length - 1];
jest.spyOn(lastFillCipherElement as HTMLElement, "focus"); jest.spyOn(lastFillCipherElement as HTMLElement, "focus");
@@ -195,7 +203,7 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard focus to the view cipher button on ArrowRight", () => { it("allows the user to move keyboard focus to the view cipher button on ArrowRight", () => {
const cipherContainerElement = const cipherContainerElement =
autofillOverlayList["overlayListContainer"].querySelector(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".cipher-container");
const fillCipherElement = cipherContainerElement.querySelector(".fill-cipher-button"); const fillCipherElement = cipherContainerElement.querySelector(".fill-cipher-button");
const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button"); const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button");
jest.spyOn(viewCipherButton as HTMLElement, "focus"); jest.spyOn(viewCipherButton as HTMLElement, "focus");
@@ -207,7 +215,7 @@ describe("AutofillOverlayList", () => {
it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => { it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => {
const fillCipherElement = const fillCipherElement =
autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
jest.spyOn(fillCipherElement as HTMLElement, "focus"); jest.spyOn(fillCipherElement as HTMLElement, "focus");
fillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowLeft" })); fillCipherElement.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowLeft" }));
@@ -218,12 +226,12 @@ describe("AutofillOverlayList", () => {
describe("view cipher button event listeners", () => { describe("view cipher button event listeners", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
}); });
it("allows the user to view a cipher on click", () => { it("allows the user to view a cipher on click", () => {
const viewCipherButton = const viewCipherButton =
autofillOverlayList["overlayListContainer"].querySelector(".view-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".view-cipher-button");
viewCipherButton.dispatchEvent(new Event("click")); viewCipherButton.dispatchEvent(new Event("click"));
@@ -235,7 +243,7 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard focus to the current cipher element on ArrowLeft", () => { it("allows the user to move keyboard focus to the current cipher element on ArrowLeft", () => {
const cipherContainerElement = const cipherContainerElement =
autofillOverlayList["overlayListContainer"].querySelector(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".cipher-container");
const fillCipherButton = cipherContainerElement.querySelector(".fill-cipher-button"); const fillCipherButton = cipherContainerElement.querySelector(".fill-cipher-button");
const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button"); const viewCipherButton = cipherContainerElement.querySelector(".view-cipher-button");
jest.spyOn(fillCipherButton as HTMLElement, "focus"); jest.spyOn(fillCipherButton as HTMLElement, "focus");
@@ -247,7 +255,7 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard to the next cipher element on ArrowDown", () => { it("allows the user to move keyboard to the next cipher element on ArrowDown", () => {
const cipherContainerElements = const cipherContainerElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(".cipher-container");
const viewCipherButton = cipherContainerElements[0].querySelector(".view-cipher-button"); const viewCipherButton = cipherContainerElements[0].querySelector(".view-cipher-button");
const secondFillCipherButton = const secondFillCipherButton =
cipherContainerElements[1].querySelector(".fill-cipher-button"); cipherContainerElements[1].querySelector(".fill-cipher-button");
@@ -260,7 +268,7 @@ describe("AutofillOverlayList", () => {
it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => { it("allows the user to move keyboard focus to the previous cipher element on ArrowUp", () => {
const cipherContainerElements = const cipherContainerElements =
autofillOverlayList["overlayListContainer"].querySelectorAll(".cipher-container"); autofillInlineMenuList["inlineMenuListContainer"].querySelectorAll(".cipher-container");
const viewCipherButton = cipherContainerElements[1].querySelector(".view-cipher-button"); const viewCipherButton = cipherContainerElements[1].querySelector(".view-cipher-button");
const firstFillCipherButton = const firstFillCipherButton =
cipherContainerElements[0].querySelector(".fill-cipher-button"); cipherContainerElements[0].querySelector(".fill-cipher-button");
@@ -273,7 +281,7 @@ describe("AutofillOverlayList", () => {
it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => { it("ignores keyup events that do not include ArrowUp, ArrowDown, or ArrowRight", () => {
const viewCipherButton = const viewCipherButton =
autofillOverlayList["overlayListContainer"].querySelector(".view-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".view-cipher-button");
jest.spyOn(viewCipherButton as HTMLElement, "focus"); jest.spyOn(viewCipherButton as HTMLElement, "focus");
viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowRight" })); viewCipherButton.dispatchEvent(new KeyboardEvent("keyup", { code: "ArrowRight" }));
@@ -286,10 +294,10 @@ describe("AutofillOverlayList", () => {
describe("global event listener handlers", () => { describe("global event listener handlers", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
}); });
it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the overlay is currently focused", () => { it("does not post a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is currently focused", () => {
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true); jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(true);
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
@@ -297,7 +305,7 @@ describe("AutofillOverlayList", () => {
expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
}); });
it("posts a `checkAutofillInlineMenuButtonFocused` message to the parent if the overlay is not currently focused", () => { it("posts a `checkAutofillInlineMenuButtonFocused` message to the parent if the inline menu is not currently focused", () => {
jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false); jest.spyOn(globalThis.document, "hasFocus").mockReturnValue(false);
postWindowMessage({ command: "checkAutofillInlineMenuListFocused" }); postWindowMessage({ command: "checkAutofillInlineMenuListFocused" });
@@ -309,43 +317,43 @@ describe("AutofillOverlayList", () => {
}); });
it("updates the list of ciphers", () => { it("updates the list of ciphers", () => {
postWindowMessage(createInitAutofillOverlayListMessageMock()); postWindowMessage(createInitAutofillInlineMenuListMessageMock());
const updateCiphersSpy = jest.spyOn(autofillOverlayList as any, "updateListItems"); const updateCiphersSpy = jest.spyOn(autofillInlineMenuList as any, "updateListItems");
postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" }); postWindowMessage({ command: "updateAutofillInlineMenuListCiphers" });
expect(updateCiphersSpy).toHaveBeenCalled(); expect(updateCiphersSpy).toHaveBeenCalled();
}); });
describe("directing user focus into the overlay list", () => { describe("directing user focus into the inline menu list", () => {
it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => { it("sets ARIA attributes that define the list as a `dialog` to screen reader users", () => {
postWindowMessage( postWindowMessage(
createInitAutofillOverlayListMessageMock({ createInitAutofillInlineMenuListMessageMock({
authStatus: AuthenticationStatus.Locked, authStatus: AuthenticationStatus.Locked,
cipherList: [], cipherList: [],
}), }),
); );
const overlayContainerSetAttributeSpy = jest.spyOn( const inlineMenuContainerSetAttributeSpy = jest.spyOn(
autofillOverlayList["overlayListContainer"], autofillInlineMenuList["inlineMenuListContainer"],
"setAttribute", "setAttribute",
); );
postWindowMessage({ command: "focusInlineMenuList" }); postWindowMessage({ command: "focusInlineMenuList" });
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog"); expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("role", "dialog");
expect(overlayContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true"); expect(inlineMenuContainerSetAttributeSpy).toHaveBeenCalledWith("aria-modal", "true");
}); });
it("focuses the unlock button element if the user is not authenticated", async () => { it("focuses the unlock button element if the user is not authenticated", async () => {
postWindowMessage( postWindowMessage(
createInitAutofillOverlayListMessageMock({ createInitAutofillInlineMenuListMessageMock({
authStatus: AuthenticationStatus.Locked, authStatus: AuthenticationStatus.Locked,
cipherList: [], cipherList: [],
}), }),
); );
await flushPromises(); await flushPromises();
const unlockButton = const unlockButton =
autofillOverlayList["overlayListContainer"].querySelector("#unlock-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector("#unlock-button");
jest.spyOn(unlockButton as HTMLElement, "focus"); jest.spyOn(unlockButton as HTMLElement, "focus");
postWindowMessage({ command: "focusInlineMenuList" }); postWindowMessage({ command: "focusInlineMenuList" });
@@ -354,10 +362,10 @@ describe("AutofillOverlayList", () => {
}); });
it("focuses the new item button element if the cipher list is empty", async () => { it("focuses the new item button element if the cipher list is empty", async () => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ ciphers: [] })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ ciphers: [] }));
await flushPromises(); await flushPromises();
const newItemButton = const newItemButton =
autofillOverlayList["overlayListContainer"].querySelector("#new-item-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector("#new-item-button");
jest.spyOn(newItemButton as HTMLElement, "focus"); jest.spyOn(newItemButton as HTMLElement, "focus");
postWindowMessage({ command: "focusInlineMenuList" }); postWindowMessage({ command: "focusInlineMenuList" });
@@ -366,9 +374,9 @@ describe("AutofillOverlayList", () => {
}); });
it("focuses the first cipher button element if the cipher list is populated", () => { it("focuses the first cipher button element if the cipher list is populated", () => {
postWindowMessage(createInitAutofillOverlayListMessageMock()); postWindowMessage(createInitAutofillInlineMenuListMessageMock());
const firstCipherItem = const firstCipherItem =
autofillOverlayList["overlayListContainer"].querySelector(".fill-cipher-button"); autofillInlineMenuList["inlineMenuListContainer"].querySelector(".fill-cipher-button");
jest.spyOn(firstCipherItem as HTMLElement, "focus"); jest.spyOn(firstCipherItem as HTMLElement, "focus");
postWindowMessage({ command: "focusInlineMenuList" }); postWindowMessage({ command: "focusInlineMenuList" });
@@ -378,8 +386,8 @@ describe("AutofillOverlayList", () => {
}); });
describe("blur event", () => { describe("blur event", () => {
it("posts a message to the parent window indicating that the overlay has lost focus", () => { it("posts a message to the parent window indicating that the inline menu has lost focus", () => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
globalThis.dispatchEvent(new Event("blur")); globalThis.dispatchEvent(new Event("blur"));
@@ -392,7 +400,7 @@ describe("AutofillOverlayList", () => {
describe("keydown event", () => { describe("keydown event", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
}); });
it("skips redirecting keyboard focus when a KeyDown event triggers and the key is not a `Tab` or `Escape` key", () => { it("skips redirecting keyboard focus when a KeyDown event triggers and the key is not a `Tab` or `Escape` key", () => {
@@ -401,7 +409,7 @@ describe("AutofillOverlayList", () => {
expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
}); });
it("redirects the overlay focus out to the previous element on KeyDown of the `Tab+Shift` keys", () => { it("redirects the inline menu focus out to the previous element on KeyDown of the `Tab+Shift` keys", () => {
globalThis.document.dispatchEvent( globalThis.document.dispatchEvent(
new KeyboardEvent("keydown", { code: "Tab", shiftKey: true }), new KeyboardEvent("keydown", { code: "Tab", shiftKey: true }),
); );
@@ -412,7 +420,7 @@ describe("AutofillOverlayList", () => {
); );
}); });
it("redirects the overlay focus out to the next element on KeyDown of the `Tab` key", () => { it("redirects the inline menu focus out to the next element on KeyDown of the `Tab` key", () => {
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" })); globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Tab" }));
expect(globalThis.parent.postMessage).toHaveBeenCalledWith( expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
@@ -421,7 +429,7 @@ describe("AutofillOverlayList", () => {
); );
}); });
it("redirects the overlay focus out to the current element on KeyDown of the `Escape` key", () => { it("redirects the inline menu focus out to the current element on KeyDown of the `Escape` key", () => {
globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" })); globalThis.document.dispatchEvent(new KeyboardEvent("keydown", { code: "Escape" }));
expect(globalThis.parent.postMessage).toHaveBeenCalledWith( expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
@@ -434,10 +442,10 @@ describe("AutofillOverlayList", () => {
describe("handleResizeObserver", () => { describe("handleResizeObserver", () => {
beforeEach(() => { beforeEach(() => {
postWindowMessage(createInitAutofillOverlayListMessageMock({ portKey })); postWindowMessage(createInitAutofillInlineMenuListMessageMock({ portKey }));
}); });
it("ignores resize entries whose target is not the overlay list", () => { it("ignores resize entries whose target is not the inline menu list", () => {
const entries = [ const entries = [
{ {
target: mock<HTMLElement>(), target: mock<HTMLElement>(),
@@ -445,20 +453,20 @@ describe("AutofillOverlayList", () => {
}, },
]; ];
autofillOverlayList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]); autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); expect(globalThis.parent.postMessage).not.toHaveBeenCalled();
}); });
it("posts a message to update the overlay list height if the list container is resized", () => { it("posts a message to update the inline menu list height if the list container is resized", () => {
const entries = [ const entries = [
{ {
target: autofillOverlayList["overlayListContainer"], target: autofillInlineMenuList["inlineMenuListContainer"],
contentRect: { height: 300 }, contentRect: { height: 300 },
}, },
]; ];
autofillOverlayList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]); autofillInlineMenuList["handleResizeObserver"](entries as unknown as ResizeObserverEntry[]);
expect(globalThis.parent.postMessage).toHaveBeenCalledWith( expect(globalThis.parent.postMessage).toHaveBeenCalledWith(
{ command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey }, { command: "updateAutofillInlineMenuListHeight", styles: { height: "300px" }, portKey },

View File

@@ -13,7 +13,7 @@ import {
import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element"; import { AutofillInlineMenuPageElement } from "../shared/autofill-inline-menu-page-element";
export class AutofillInlineMenuList extends AutofillInlineMenuPageElement { export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private overlayListContainer: HTMLDivElement; private inlineMenuListContainer: HTMLDivElement;
private resizeObserver: ResizeObserver; private resizeObserver: ResizeObserver;
private eventHandlersMemo: { [key: string]: EventListener } = {}; private eventHandlersMemo: { [key: string]: EventListener } = {};
private ciphers: OverlayCipherData[] = []; private ciphers: OverlayCipherData[] = [];
@@ -22,7 +22,8 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout; private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
private currentCipherIndex = 0; private currentCipherIndex = 0;
private readonly showCiphersPerPage = 6; private readonly showCiphersPerPage = 6;
private readonly overlayListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers = { private readonly inlineMenuListWindowMessageHandlers: AutofillInlineMenuListWindowMessageHandlers =
{
initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message), initAutofillInlineMenuList: ({ message }) => this.initAutofillInlineMenuList(message),
checkAutofillInlineMenuListFocused: () => this.checkInlineMenuListFocused(), checkAutofillInlineMenuListFocused: () => this.checkInlineMenuListFocused(),
updateAutofillInlineMenuListCiphers: ({ message }) => this.updateListItems(message.ciphers), updateAutofillInlineMenuListCiphers: ({ message }) => this.updateListItems(message.ciphers),
@@ -32,18 +33,18 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
constructor() { constructor() {
super(); super();
this.setupOverlayListGlobalListeners(); this.setupInlineMenuListGlobalListeners();
} }
/** /**
* Initializes the overlay list and updates the list items with the passed ciphers. * Initializes the inline menu list and updates the list items with the passed ciphers.
* If the auth status is not `Unlocked`, the locked overlay is built. * If the auth status is not `Unlocked`, the locked inline menu is built.
* *
* @param translations - The translations to use for the overlay list. * @param translations - The translations to use for the inline menu list.
* @param styleSheetUrl - The URL of the stylesheet to use for the overlay list. * @param styleSheetUrl - The URL of the stylesheet to use for the inline menu list.
* @param theme - The theme to use for the overlay list. * @param theme - The theme to use for the inline menu list.
* @param authStatus - The current authentication status. * @param authStatus - The current authentication status.
* @param ciphers - The ciphers to display in the overlay list. * @param ciphers - The ciphers to display in the inline menu list.
* @param portKey - Background generated key that allows the port to communicate with the background. * @param portKey - Background generated key that allows the port to communicate with the background.
*/ */
private async initAutofillInlineMenuList({ private async initAutofillInlineMenuList({
@@ -64,34 +65,34 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
const themeClass = `theme_${theme}`; const themeClass = `theme_${theme}`;
globalThis.document.documentElement.classList.add(themeClass); globalThis.document.documentElement.classList.add(themeClass);
this.overlayListContainer = globalThis.document.createElement("div"); this.inlineMenuListContainer = globalThis.document.createElement("div");
this.overlayListContainer.classList.add("overlay-list-container", themeClass); this.inlineMenuListContainer.classList.add("inline-menu-list-container", themeClass);
this.resizeObserver.observe(this.overlayListContainer); this.resizeObserver.observe(this.inlineMenuListContainer);
this.shadowDom.append(linkElement, this.overlayListContainer); this.shadowDom.append(linkElement, this.inlineMenuListContainer);
if (authStatus === AuthenticationStatus.Unlocked) { if (authStatus === AuthenticationStatus.Unlocked) {
this.updateListItems(ciphers); this.updateListItems(ciphers);
return; return;
} }
this.buildLockedOverlay(); this.buildLockedInlineMenu();
} }
/** /**
* Builds the locked overlay, which is displayed when the user is not authenticated. * Builds the locked inline menu, which is displayed when the user is not authenticated.
* Facilitates the ability to unlock the extension from the overlay. * Facilitates the ability to unlock the extension from the inline menu.
*/ */
private buildLockedOverlay() { private buildLockedInlineMenu() {
const lockedOverlay = globalThis.document.createElement("div"); const lockedInlineMenu = globalThis.document.createElement("div");
lockedOverlay.id = "locked-overlay-description"; lockedInlineMenu.id = "locked-inline-menu-description";
lockedOverlay.classList.add("locked-overlay", "overlay-list-message"); lockedInlineMenu.classList.add("locked-inline-menu", "inline-menu-list-message");
lockedOverlay.textContent = this.getTranslation("unlockYourAccount"); lockedInlineMenu.textContent = this.getTranslation("unlockYourAccount");
const unlockButtonElement = globalThis.document.createElement("button"); const unlockButtonElement = globalThis.document.createElement("button");
unlockButtonElement.id = "unlock-button"; unlockButtonElement.id = "unlock-button";
unlockButtonElement.tabIndex = -1; unlockButtonElement.tabIndex = -1;
unlockButtonElement.classList.add("unlock-button", "overlay-list-button"); unlockButtonElement.classList.add("unlock-button", "inline-menu-list-button");
unlockButtonElement.textContent = this.getTranslation("unlockAccount"); unlockButtonElement.textContent = this.getTranslation("unlockAccount");
unlockButtonElement.setAttribute( unlockButtonElement.setAttribute(
"aria-label", "aria-label",
@@ -100,11 +101,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
unlockButtonElement.prepend(buildSvgDomElement(lockIcon)); unlockButtonElement.prepend(buildSvgDomElement(lockIcon));
unlockButtonElement.addEventListener(EVENTS.CLICK, this.handleUnlockButtonClick); unlockButtonElement.addEventListener(EVENTS.CLICK, this.handleUnlockButtonClick);
const overlayListButtonContainer = globalThis.document.createElement("div"); const inlineMenuListButtonContainer = globalThis.document.createElement("div");
overlayListButtonContainer.classList.add("overlay-list-button-container"); inlineMenuListButtonContainer.classList.add("inline-menu-list-button-container");
overlayListButtonContainer.appendChild(unlockButtonElement); inlineMenuListButtonContainer.appendChild(unlockButtonElement);
this.overlayListContainer.append(lockedOverlay, overlayListButtonContainer); this.inlineMenuListContainer.append(lockedInlineMenu, inlineMenuListButtonContainer);
} }
/** /**
@@ -117,45 +118,45 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
/** /**
* Updates the list items with the passed ciphers. * Updates the list items with the passed ciphers.
* If no ciphers are passed, the no results overlay is built. * If no ciphers are passed, the no results inline menu is built.
* *
* @param ciphers - The ciphers to display in the overlay list. * @param ciphers - The ciphers to display in the inline menu list.
*/ */
private updateListItems(ciphers: OverlayCipherData[]) { private updateListItems(ciphers: OverlayCipherData[]) {
this.ciphers = ciphers; this.ciphers = ciphers;
this.currentCipherIndex = 0; this.currentCipherIndex = 0;
if (this.overlayListContainer) { if (this.inlineMenuListContainer) {
this.overlayListContainer.innerHTML = ""; this.inlineMenuListContainer.innerHTML = "";
} }
if (!ciphers?.length) { if (!ciphers?.length) {
this.buildNoResultsOverlayList(); this.buildNoResultsInlineMenuList();
return; return;
} }
this.ciphersList = globalThis.document.createElement("ul"); this.ciphersList = globalThis.document.createElement("ul");
this.ciphersList.classList.add("overlay-actions-list"); this.ciphersList.classList.add("inline-menu-list-actions");
this.ciphersList.setAttribute("role", "list"); this.ciphersList.setAttribute("role", "list");
globalThis.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent); globalThis.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
this.loadPageOfCiphers(); this.loadPageOfCiphers();
this.overlayListContainer.appendChild(this.ciphersList); this.inlineMenuListContainer.appendChild(this.ciphersList);
} }
/** /**
* Overlay view that is presented when no ciphers are found for a given page. * Inline menu view that is presented when no ciphers are found for a given page.
* Facilitates the ability to add a new vault item from the overlay. * Facilitates the ability to add a new vault item from the inline menu.
*/ */
private buildNoResultsOverlayList() { private buildNoResultsInlineMenuList() {
const noItemsMessage = globalThis.document.createElement("div"); const noItemsMessage = globalThis.document.createElement("div");
noItemsMessage.classList.add("no-items", "overlay-list-message"); noItemsMessage.classList.add("no-items", "inline-menu-list-message");
noItemsMessage.textContent = this.getTranslation("noItemsToShow"); noItemsMessage.textContent = this.getTranslation("noItemsToShow");
const newItemButton = globalThis.document.createElement("button"); const newItemButton = globalThis.document.createElement("button");
newItemButton.tabIndex = -1; newItemButton.tabIndex = -1;
newItemButton.id = "new-item-button"; newItemButton.id = "new-item-button";
newItemButton.classList.add("add-new-item-button", "overlay-list-button"); newItemButton.classList.add("add-new-item-button", "inline-menu-list-button");
newItemButton.textContent = this.getTranslation("newItem"); newItemButton.textContent = this.getTranslation("newItem");
newItemButton.setAttribute( newItemButton.setAttribute(
"aria-label", "aria-label",
@@ -164,11 +165,11 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
newItemButton.prepend(buildSvgDomElement(plusIcon)); newItemButton.prepend(buildSvgDomElement(plusIcon));
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick); newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
const overlayListButtonContainer = globalThis.document.createElement("div"); const inlineMenuListButtonContainer = globalThis.document.createElement("div");
overlayListButtonContainer.classList.add("overlay-list-button-container"); inlineMenuListButtonContainer.classList.add("inline-menu-list-button-container");
overlayListButtonContainer.appendChild(newItemButton); inlineMenuListButtonContainer.appendChild(newItemButton);
this.overlayListContainer.append(noItemsMessage, overlayListButtonContainer); this.inlineMenuListContainer.append(noItemsMessage, inlineMenuListButtonContainer);
} }
/** /**
@@ -180,7 +181,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}; };
/** /**
* Loads a page of ciphers into the overlay list container. * Loads a page of ciphers into the inline menu list container.
*/ */
private loadPageOfCiphers() { private loadPageOfCiphers() {
const lastIndex = Math.min( const lastIndex = Math.min(
@@ -188,7 +189,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
this.ciphers.length, this.ciphers.length,
); );
for (let cipherIndex = this.currentCipherIndex; cipherIndex < lastIndex; cipherIndex++) { for (let cipherIndex = this.currentCipherIndex; cipherIndex < lastIndex; cipherIndex++) {
this.ciphersList.appendChild(this.buildOverlayActionsListItem(this.ciphers[cipherIndex])); this.ciphersList.appendChild(this.buildInlineMenuListActionsItem(this.ciphers[cipherIndex]));
this.currentCipherIndex++; this.currentCipherIndex++;
} }
@@ -230,7 +231,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
* *
* @param cipher - The cipher to build the list item for. * @param cipher - The cipher to build the list item for.
*/ */
private buildOverlayActionsListItem(cipher: OverlayCipherData) { private buildInlineMenuListActionsItem(cipher: OverlayCipherData) {
const fillCipherElement = this.buildFillCipherElement(cipher); const fillCipherElement = this.buildFillCipherElement(cipher);
const viewCipherElement = this.buildViewCipherElement(cipher); const viewCipherElement = this.buildViewCipherElement(cipher);
@@ -238,12 +239,12 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
cipherContainerElement.classList.add("cipher-container"); cipherContainerElement.classList.add("cipher-container");
cipherContainerElement.append(fillCipherElement, viewCipherElement); cipherContainerElement.append(fillCipherElement, viewCipherElement);
const overlayActionsListItem = globalThis.document.createElement("li"); const inlineMenuListActionsItem = globalThis.document.createElement("li");
overlayActionsListItem.setAttribute("role", "listitem"); inlineMenuListActionsItem.setAttribute("role", "listitem");
overlayActionsListItem.classList.add("overlay-actions-list-item"); inlineMenuListActionsItem.classList.add("inline-menu-list-actions-item");
overlayActionsListItem.appendChild(cipherContainerElement); inlineMenuListActionsItem.appendChild(cipherContainerElement);
return overlayActionsListItem; return inlineMenuListActionsItem;
} }
/** /**
@@ -306,7 +307,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
event.preventDefault(); event.preventDefault();
const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement; const currentListItem = event.target.closest(".inline-menu-list-actions-item") as HTMLElement;
if (event.code === "ArrowDown") { if (event.code === "ArrowDown") {
this.focusNextListItem(currentListItem); this.focusNextListItem(currentListItem);
return; return;
@@ -369,7 +370,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
event.preventDefault(); event.preventDefault();
const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement; const currentListItem = event.target.closest(".inline-menu-list-actions-item") as HTMLElement;
const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement; const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement;
cipherContainer?.classList.remove("remove-outline"); cipherContainer?.classList.remove("remove-outline");
if (event.code === "ArrowDown") { if (event.code === "ArrowDown") {
@@ -484,7 +485,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
} }
/** /**
* Validates whether the overlay list iframe is currently focused. * Validates whether the inline menu list iframe is currently focused.
* If not focused, will check if the button element is focused. * If not focused, will check if the button element is focused.
*/ */
private checkInlineMenuListFocused() { private checkInlineMenuListFocused() {
@@ -496,15 +497,15 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
} }
/** /**
* Focuses the overlay list iframe. The element that receives focus is * Focuses the inline menu list iframe. The element that receives focus is
* determined by the presence of the unlock button, new item button, or * determined by the presence of the unlock button, new item button, or
* the first cipher button. * the first cipher button.
*/ */
private focusInlineMenuList() { private focusInlineMenuList() {
this.overlayListContainer.setAttribute("role", "dialog"); this.inlineMenuListContainer.setAttribute("role", "dialog");
this.overlayListContainer.setAttribute("aria-modal", "true"); this.inlineMenuListContainer.setAttribute("aria-modal", "true");
const unlockButtonElement = this.overlayListContainer.querySelector( const unlockButtonElement = this.inlineMenuListContainer.querySelector(
"#unlock-button", "#unlock-button",
) as HTMLElement; ) as HTMLElement;
if (unlockButtonElement) { if (unlockButtonElement) {
@@ -512,7 +513,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return; return;
} }
const newItemButtonElement = this.overlayListContainer.querySelector( const newItemButtonElement = this.inlineMenuListContainer.querySelector(
"#new-item-button", "#new-item-button",
) as HTMLElement; ) as HTMLElement;
if (newItemButtonElement) { if (newItemButtonElement) {
@@ -520,31 +521,31 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
return; return;
} }
const firstCipherElement = this.overlayListContainer.querySelector( const firstCipherElement = this.inlineMenuListContainer.querySelector(
".fill-cipher-button", ".fill-cipher-button",
) as HTMLElement; ) as HTMLElement;
firstCipherElement?.focus(); firstCipherElement?.focus();
} }
/** /**
* Sets up the global listeners for the overlay list iframe. * Sets up the global listeners for the inline menu list iframe.
*/ */
private setupOverlayListGlobalListeners() { private setupInlineMenuListGlobalListeners() {
this.setupGlobalListeners(this.overlayListWindowMessageHandlers); this.setupGlobalListeners(this.inlineMenuListWindowMessageHandlers);
this.resizeObserver = new ResizeObserver(this.handleResizeObserver); this.resizeObserver = new ResizeObserver(this.handleResizeObserver);
} }
/** /**
* Handles the resize observer event. Facilitates updating the height of the * Handles the resize observer event. Facilitates updating the height of the
* overlay list iframe when the height of the list changes. * inline menu list iframe when the height of the list changes.
* *
* @param entries - The resize observer entries. * @param entries - The resize observer entries.
*/ */
private handleResizeObserver = (entries: ResizeObserverEntry[]) => { private handleResizeObserver = (entries: ResizeObserverEntry[]) => {
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) { for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
const entry = entries[entryIndex]; const entry = entries[entryIndex];
if (entry.target !== this.overlayListContainer) { if (entry.target !== this.inlineMenuListContainer) {
continue; continue;
} }
@@ -568,7 +569,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
}; };
/** /**
* Focuses the next list item in the overlay list. If the current list item is the last * Focuses the next list item in the inline menu list. If the current list item is the last
* item in the list, the first item is focused. * item in the list, the first item is focused.
* *
* @param currentListItem - The current list item. * @param currentListItem - The current list item.
@@ -587,7 +588,7 @@ export class AutofillInlineMenuList extends AutofillInlineMenuPageElement {
} }
/** /**
* Focuses the previous list item in the overlay list. If the current list item is the first * Focuses the previous list item in the inline menu list. If the current list item is the first
* item in the list, the last item is focused. * item in the list, the last item is focused.
* *
* @param currentListItem - The current list item. * @param currentListItem - The current list item.

View File

@@ -22,7 +22,7 @@ body {
} }
} }
.overlay-list-message { .inline-menu-list-message {
font-family: $font-family-sans-serif; font-family: $font-family-sans-serif;
font-weight: 400; font-weight: 400;
font-size: 1.4rem; font-size: 1.4rem;
@@ -39,7 +39,7 @@ body {
} }
} }
.overlay-list-button-container { .inline-menu-list-button-container {
width: 100%; width: 100%;
padding: 0.2rem; padding: 0.2rem;
background: transparent; background: transparent;
@@ -58,7 +58,7 @@ body {
} }
} }
.overlay-list-button { .inline-menu-list-button {
display: flex; display: flex;
align-content: center; align-content: center;
justify-content: flex-start; justify-content: flex-start;
@@ -116,12 +116,12 @@ body {
} }
} }
.overlay-actions-list { .inline-menu-list-actions {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.overlay-actions-list-item { .inline-menu-list-actions-item {
transition: background-color 0.2s ease-in-out; transition: background-color 0.2s ease-in-out;
list-style: none; list-style: none;
padding: 0.2rem; padding: 0.2rem;

View File

@@ -195,7 +195,7 @@ function createAutofillOverlayCipherDataMock(index: number, customFields = {}):
}; };
} }
function createInitAutofillOverlayListMessageMock( function createInitAutofillInlineMenuListMessageMock(
customFields = {}, customFields = {},
): InitAutofillInlineMenuListMessage { ): InitAutofillInlineMenuListMessage {
return { return {
@@ -298,7 +298,7 @@ export {
createGenerateFillScriptOptionsMock, createGenerateFillScriptOptionsMock,
createAutofillScriptMock, createAutofillScriptMock,
createInitAutofillInlineMenuButtonMessageMock, createInitAutofillInlineMenuButtonMessageMock,
createInitAutofillOverlayListMessageMock, createInitAutofillInlineMenuListMessageMock,
createFocusedFieldDataMock, createFocusedFieldDataMock,
createPortSpyMock, createPortSpyMock,
createMutationRecordMock, createMutationRecordMock,