604 lines
21 KiB
TypeScript
604 lines
21 KiB
TypeScript
import "@webcomponents/custom-elements";
|
|
import "lit/polyfill-support.js";
|
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
|
|
|
import { OverlayCipherData } from "../../../background/abstractions/overlay.background";
|
|
import { EVENTS } from "../../../constants";
|
|
import { globeIcon, lockIcon, plusIcon, viewCipherIcon } from "../../../utils/svg-icons";
|
|
import { buildSvgDomElement } from "../../../utils/utils";
|
|
import {
|
|
InitAutofillOverlayListMessage,
|
|
OverlayListWindowMessageHandlers,
|
|
} from "../../abstractions/autofill-overlay-list";
|
|
import AutofillOverlayPageElement from "../shared/autofill-overlay-page-element";
|
|
|
|
class AutofillOverlayList extends AutofillOverlayPageElement {
|
|
private overlayListContainer: HTMLDivElement;
|
|
private resizeObserver: ResizeObserver;
|
|
private eventHandlersMemo: { [key: string]: EventListener } = {};
|
|
private ciphers: OverlayCipherData[] = [];
|
|
private ciphersList: HTMLUListElement;
|
|
private cipherListScrollIsDebounced = false;
|
|
private cipherListScrollDebounceTimeout: NodeJS.Timeout;
|
|
private currentCipherIndex = 0;
|
|
private readonly showCiphersPerPage = 6;
|
|
private readonly overlayListWindowMessageHandlers: OverlayListWindowMessageHandlers = {
|
|
initAutofillOverlayList: ({ message }) => this.initAutofillOverlayList(message),
|
|
checkAutofillOverlayListFocused: () => this.checkOverlayListFocused(),
|
|
updateOverlayListCiphers: ({ message }) => this.updateListItems(message.ciphers),
|
|
focusOverlayList: () => this.focusOverlayList(),
|
|
};
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.setupOverlayListGlobalListeners();
|
|
}
|
|
|
|
/**
|
|
* Initializes the overlay list and updates the list items with the passed ciphers.
|
|
* If the auth status is not `Unlocked`, the locked overlay is built.
|
|
*
|
|
* @param translations - The translations to use for the overlay list.
|
|
* @param styleSheetUrl - The URL of the stylesheet to use for the overlay list.
|
|
* @param theme - The theme to use for the overlay list.
|
|
* @param authStatus - The current authentication status.
|
|
* @param ciphers - The ciphers to display in the overlay list.
|
|
*/
|
|
private async initAutofillOverlayList({
|
|
translations,
|
|
styleSheetUrl,
|
|
theme,
|
|
authStatus,
|
|
ciphers,
|
|
}: InitAutofillOverlayListMessage) {
|
|
const linkElement = this.initOverlayPage("button", styleSheetUrl, translations);
|
|
|
|
globalThis.document.documentElement.classList.add(theme);
|
|
|
|
this.overlayListContainer = globalThis.document.createElement("div");
|
|
this.overlayListContainer.classList.add("overlay-list-container", theme);
|
|
this.overlayListContainer.setAttribute("role", "dialog");
|
|
this.overlayListContainer.setAttribute("aria-modal", "true");
|
|
this.resizeObserver.observe(this.overlayListContainer);
|
|
|
|
this.shadowDom.append(linkElement, this.overlayListContainer);
|
|
|
|
if (authStatus === AuthenticationStatus.Unlocked) {
|
|
this.updateListItems(ciphers);
|
|
return;
|
|
}
|
|
|
|
this.buildLockedOverlay();
|
|
}
|
|
|
|
/**
|
|
* Builds the locked overlay, which is displayed when the user is not authenticated.
|
|
* Facilitates the ability to unlock the extension from the overlay.
|
|
*/
|
|
private buildLockedOverlay() {
|
|
const lockedOverlay = globalThis.document.createElement("div");
|
|
lockedOverlay.id = "locked-overlay-description";
|
|
lockedOverlay.classList.add("locked-overlay", "overlay-list-message");
|
|
lockedOverlay.textContent = this.getTranslation("unlockYourAccount");
|
|
|
|
const unlockButtonElement = globalThis.document.createElement("button");
|
|
unlockButtonElement.id = "unlock-button";
|
|
unlockButtonElement.tabIndex = -1;
|
|
unlockButtonElement.classList.add("unlock-button", "overlay-list-button");
|
|
unlockButtonElement.textContent = this.getTranslation("unlockAccount");
|
|
unlockButtonElement.setAttribute(
|
|
"aria-label",
|
|
`${this.getTranslation("unlockAccount")}, ${this.getTranslation("opensInANewWindow")}`,
|
|
);
|
|
unlockButtonElement.prepend(buildSvgDomElement(lockIcon));
|
|
unlockButtonElement.addEventListener(EVENTS.CLICK, this.handleUnlockButtonClick);
|
|
|
|
const overlayListButtonContainer = globalThis.document.createElement("div");
|
|
overlayListButtonContainer.classList.add("overlay-list-button-container");
|
|
overlayListButtonContainer.appendChild(unlockButtonElement);
|
|
|
|
this.overlayListContainer.append(lockedOverlay, overlayListButtonContainer);
|
|
}
|
|
|
|
/**
|
|
* Handles the click event for the unlock button.
|
|
* Sends a message to the parent window to unlock the vault.
|
|
*/
|
|
private handleUnlockButtonClick = () => {
|
|
this.postMessageToParent({ command: "unlockVault" });
|
|
};
|
|
|
|
/**
|
|
* Updates the list items with the passed ciphers.
|
|
* If no ciphers are passed, the no results overlay is built.
|
|
*
|
|
* @param ciphers - The ciphers to display in the overlay list.
|
|
*/
|
|
private updateListItems(ciphers: OverlayCipherData[]) {
|
|
this.ciphers = ciphers;
|
|
this.currentCipherIndex = 0;
|
|
this.overlayListContainer.innerHTML = "";
|
|
|
|
if (!ciphers?.length) {
|
|
this.buildNoResultsOverlayList();
|
|
return;
|
|
}
|
|
|
|
this.ciphersList = globalThis.document.createElement("ul");
|
|
this.ciphersList.classList.add("overlay-actions-list");
|
|
this.ciphersList.setAttribute("role", "list");
|
|
globalThis.addEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
|
|
|
|
this.loadPageOfCiphers();
|
|
|
|
this.overlayListContainer.appendChild(this.ciphersList);
|
|
}
|
|
|
|
/**
|
|
* Overlay 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.
|
|
*/
|
|
private buildNoResultsOverlayList() {
|
|
const noItemsMessage = globalThis.document.createElement("div");
|
|
noItemsMessage.classList.add("no-items", "overlay-list-message");
|
|
noItemsMessage.textContent = this.getTranslation("noItemsToShow");
|
|
|
|
const newItemButton = globalThis.document.createElement("button");
|
|
newItemButton.tabIndex = -1;
|
|
newItemButton.id = "new-item-button";
|
|
newItemButton.classList.add("add-new-item-button", "overlay-list-button");
|
|
newItemButton.textContent = this.getTranslation("newItem");
|
|
newItemButton.setAttribute(
|
|
"aria-label",
|
|
`${this.getTranslation("addNewVaultItem")}, ${this.getTranslation("opensInANewWindow")}`,
|
|
);
|
|
newItemButton.prepend(buildSvgDomElement(plusIcon));
|
|
newItemButton.addEventListener(EVENTS.CLICK, this.handeNewItemButtonClick);
|
|
|
|
const overlayListButtonContainer = globalThis.document.createElement("div");
|
|
overlayListButtonContainer.classList.add("overlay-list-button-container");
|
|
overlayListButtonContainer.appendChild(newItemButton);
|
|
|
|
this.overlayListContainer.append(noItemsMessage, overlayListButtonContainer);
|
|
}
|
|
|
|
/**
|
|
* Handles the click event for the new item button.
|
|
* Sends a message to the parent window to add a new vault item.
|
|
*/
|
|
private handeNewItemButtonClick = () => {
|
|
this.postMessageToParent({ command: "addNewVaultItem" });
|
|
};
|
|
|
|
/**
|
|
* Loads a page of ciphers into the overlay list container.
|
|
*/
|
|
private loadPageOfCiphers() {
|
|
const lastIndex = Math.min(
|
|
this.currentCipherIndex + this.showCiphersPerPage,
|
|
this.ciphers.length,
|
|
);
|
|
for (let cipherIndex = this.currentCipherIndex; cipherIndex < lastIndex; cipherIndex++) {
|
|
this.ciphersList.appendChild(this.buildOverlayActionsListItem(this.ciphers[cipherIndex]));
|
|
this.currentCipherIndex++;
|
|
}
|
|
|
|
if (this.currentCipherIndex >= this.ciphers.length) {
|
|
globalThis.removeEventListener(EVENTS.SCROLL, this.handleCiphersListScrollEvent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles updating the list of ciphers when the
|
|
* user scrolls to the bottom of the list.
|
|
*/
|
|
private handleCiphersListScrollEvent = () => {
|
|
if (this.cipherListScrollIsDebounced) {
|
|
return;
|
|
}
|
|
|
|
this.cipherListScrollIsDebounced = true;
|
|
if (this.cipherListScrollDebounceTimeout) {
|
|
clearTimeout(this.cipherListScrollDebounceTimeout);
|
|
}
|
|
this.cipherListScrollDebounceTimeout = setTimeout(this.handleDebouncedScrollEvent, 300);
|
|
};
|
|
|
|
/**
|
|
* Debounced handler for updating the list of ciphers when the user scrolls to
|
|
* the bottom of the list. Triggers at most once every 300ms.
|
|
*/
|
|
private handleDebouncedScrollEvent = () => {
|
|
this.cipherListScrollIsDebounced = false;
|
|
|
|
if (globalThis.scrollY + globalThis.innerHeight >= this.ciphersList.clientHeight - 300) {
|
|
this.loadPageOfCiphers();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds the list item for a given cipher.
|
|
*
|
|
* @param cipher - The cipher to build the list item for.
|
|
*/
|
|
private buildOverlayActionsListItem(cipher: OverlayCipherData) {
|
|
const fillCipherElement = this.buildFillCipherElement(cipher);
|
|
const viewCipherElement = this.buildViewCipherElement(cipher);
|
|
|
|
const cipherContainerElement = globalThis.document.createElement("div");
|
|
cipherContainerElement.classList.add("cipher-container");
|
|
cipherContainerElement.append(fillCipherElement, viewCipherElement);
|
|
|
|
const overlayActionsListItem = globalThis.document.createElement("li");
|
|
overlayActionsListItem.setAttribute("role", "listitem");
|
|
overlayActionsListItem.classList.add("overlay-actions-list-item");
|
|
overlayActionsListItem.appendChild(cipherContainerElement);
|
|
|
|
return overlayActionsListItem;
|
|
}
|
|
|
|
/**
|
|
* Builds the fill cipher button for a given cipher.
|
|
* Wraps the cipher icon and details.
|
|
*
|
|
* @param cipher - The cipher to build the fill cipher button for.
|
|
*/
|
|
private buildFillCipherElement(cipher: OverlayCipherData) {
|
|
const cipherIcon = this.buildCipherIconElement(cipher);
|
|
const cipherDetailsElement = this.buildCipherDetailsElement(cipher);
|
|
|
|
const fillCipherElement = globalThis.document.createElement("button");
|
|
fillCipherElement.tabIndex = -1;
|
|
fillCipherElement.classList.add("fill-cipher-button");
|
|
fillCipherElement.setAttribute(
|
|
"aria-label",
|
|
`${this.getTranslation("fillCredentialsFor")} ${cipher.name}`,
|
|
);
|
|
fillCipherElement.setAttribute(
|
|
"aria-description",
|
|
`${this.getTranslation("partialUsername")}, ${cipher.login.username}`,
|
|
);
|
|
fillCipherElement.append(cipherIcon, cipherDetailsElement);
|
|
fillCipherElement.addEventListener(EVENTS.CLICK, this.handleFillCipherClickEvent(cipher));
|
|
fillCipherElement.addEventListener(EVENTS.KEYUP, this.handleFillCipherKeyUpEvent);
|
|
|
|
return fillCipherElement;
|
|
}
|
|
|
|
/**
|
|
* Handles the click event for the fill cipher button.
|
|
* Sends a message to the parent window to fill the selected cipher.
|
|
*
|
|
* @param cipher - The cipher to fill.
|
|
*/
|
|
private handleFillCipherClickEvent = (cipher: OverlayCipherData) => {
|
|
return this.useEventHandlersMemo(
|
|
() =>
|
|
this.postMessageToParent({
|
|
command: "fillSelectedListItem",
|
|
overlayCipherId: cipher.id,
|
|
}),
|
|
`${cipher.id}-fill-cipher-button-click-handler`,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Handles the keyup event for the fill cipher button. Facilitates
|
|
* selecting the next/previous cipher item on ArrowDown/ArrowUp. Also
|
|
* facilitates moving keyboard focus to the view cipher button on ArrowRight.
|
|
*
|
|
* @param event - The keyup event.
|
|
*/
|
|
private handleFillCipherKeyUpEvent = (event: KeyboardEvent) => {
|
|
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowRight"]);
|
|
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement;
|
|
if (event.code === "ArrowDown") {
|
|
this.focusNextListItem(currentListItem);
|
|
return;
|
|
}
|
|
|
|
if (event.code === "ArrowUp") {
|
|
this.focusPreviousListItem(currentListItem);
|
|
return;
|
|
}
|
|
|
|
this.focusViewCipherButton(currentListItem, event.target as HTMLElement);
|
|
};
|
|
|
|
/**
|
|
* Builds the button that facilitates viewing a cipher in the vault.
|
|
*
|
|
* @param cipher - The cipher to view.
|
|
*/
|
|
private buildViewCipherElement(cipher: OverlayCipherData) {
|
|
const viewCipherElement = globalThis.document.createElement("button");
|
|
viewCipherElement.tabIndex = -1;
|
|
viewCipherElement.classList.add("view-cipher-button");
|
|
viewCipherElement.setAttribute(
|
|
"aria-label",
|
|
`${this.getTranslation("view")} ${cipher.name}, ${this.getTranslation("opensInANewWindow")}`,
|
|
);
|
|
viewCipherElement.append(buildSvgDomElement(viewCipherIcon));
|
|
viewCipherElement.addEventListener(EVENTS.CLICK, this.handleViewCipherClickEvent(cipher));
|
|
viewCipherElement.addEventListener(EVENTS.KEYUP, this.handleViewCipherKeyUpEvent);
|
|
|
|
return viewCipherElement;
|
|
}
|
|
|
|
/**
|
|
* Handles the click event for the view cipher button. Sends a
|
|
* message to the parent window to view the selected cipher.
|
|
*
|
|
* @param cipher - The cipher to view.
|
|
*/
|
|
private handleViewCipherClickEvent = (cipher: OverlayCipherData) => {
|
|
return this.useEventHandlersMemo(
|
|
() => this.postMessageToParent({ command: "viewSelectedCipher", overlayCipherId: cipher.id }),
|
|
`${cipher.id}-view-cipher-button-click-handler`,
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Handles the keyup event for the view cipher button. Facilitates
|
|
* selecting the next/previous cipher item on ArrowDown/ArrowUp.
|
|
* Also facilitates moving keyboard focus to the current fill
|
|
* cipher button on ArrowLeft.
|
|
*
|
|
* @param event - The keyup event.
|
|
*/
|
|
private handleViewCipherKeyUpEvent = (event: KeyboardEvent) => {
|
|
const listenedForKeys = new Set(["ArrowDown", "ArrowUp", "ArrowLeft"]);
|
|
if (!listenedForKeys.has(event.code) || !(event.target instanceof Element)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const currentListItem = event.target.closest(".overlay-actions-list-item") as HTMLElement;
|
|
const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement;
|
|
cipherContainer?.classList.remove("remove-outline");
|
|
if (event.code === "ArrowDown") {
|
|
this.focusNextListItem(currentListItem);
|
|
return;
|
|
}
|
|
|
|
if (event.code === "ArrowUp") {
|
|
this.focusPreviousListItem(currentListItem);
|
|
return;
|
|
}
|
|
|
|
const previousSibling = event.target.previousElementSibling as HTMLElement;
|
|
previousSibling?.focus();
|
|
};
|
|
|
|
/**
|
|
* Builds the icon for a given cipher. Prioritizes the favicon from a given cipher url
|
|
* and the default icon element within the extension. If neither are available, the
|
|
* globe icon is used.
|
|
*
|
|
* @param cipher - The cipher to build the icon for.
|
|
*/
|
|
private buildCipherIconElement(cipher: OverlayCipherData) {
|
|
const cipherIcon = globalThis.document.createElement("span");
|
|
cipherIcon.classList.add("cipher-icon");
|
|
cipherIcon.setAttribute("aria-hidden", "true");
|
|
|
|
if (cipher.icon?.image) {
|
|
try {
|
|
const url = new URL(cipher.icon.image);
|
|
cipherIcon.style.backgroundImage = `url(${url.href})`;
|
|
return cipherIcon;
|
|
} catch {
|
|
// Silently default to the globe icon element if the image URL is invalid
|
|
}
|
|
}
|
|
|
|
if (cipher.icon?.icon) {
|
|
cipherIcon.classList.add("cipher-icon", "bwi", cipher.icon.icon);
|
|
return cipherIcon;
|
|
}
|
|
|
|
cipherIcon.append(buildSvgDomElement(globeIcon));
|
|
return cipherIcon;
|
|
}
|
|
|
|
/**
|
|
* Builds the details for a given cipher. Includes the cipher name and username login.
|
|
*
|
|
* @param cipher - The cipher to build the details for.
|
|
*/
|
|
private buildCipherDetailsElement(cipher: OverlayCipherData) {
|
|
const cipherNameElement = this.buildCipherNameElement(cipher);
|
|
const cipherUserLoginElement = this.buildCipherUserLoginElement(cipher);
|
|
|
|
const cipherDetailsElement = globalThis.document.createElement("span");
|
|
cipherDetailsElement.classList.add("cipher-details");
|
|
if (cipherNameElement) {
|
|
cipherDetailsElement.appendChild(cipherNameElement);
|
|
}
|
|
if (cipherUserLoginElement) {
|
|
cipherDetailsElement.appendChild(cipherUserLoginElement);
|
|
}
|
|
|
|
return cipherDetailsElement;
|
|
}
|
|
|
|
/**
|
|
* Builds the name element for a given cipher.
|
|
*
|
|
* @param cipher - The cipher to build the name element for.
|
|
*/
|
|
private buildCipherNameElement(cipher: OverlayCipherData): HTMLSpanElement | null {
|
|
if (!cipher.name) {
|
|
return null;
|
|
}
|
|
|
|
const cipherNameElement = globalThis.document.createElement("span");
|
|
cipherNameElement.classList.add("cipher-name");
|
|
cipherNameElement.textContent = cipher.name;
|
|
cipherNameElement.setAttribute("title", cipher.name);
|
|
|
|
return cipherNameElement;
|
|
}
|
|
|
|
/**
|
|
* Builds the username login element for a given cipher.
|
|
*
|
|
* @param cipher - The cipher to build the username login element for.
|
|
*/
|
|
private buildCipherUserLoginElement(cipher: OverlayCipherData): HTMLSpanElement | null {
|
|
if (!cipher.login?.username) {
|
|
return null;
|
|
}
|
|
|
|
const cipherUserLoginElement = globalThis.document.createElement("span");
|
|
cipherUserLoginElement.classList.add("cipher-user-login");
|
|
cipherUserLoginElement.textContent = cipher.login.username;
|
|
cipherUserLoginElement.setAttribute("title", cipher.login.username);
|
|
|
|
return cipherUserLoginElement;
|
|
}
|
|
|
|
/**
|
|
* Validates whether the overlay list iframe is currently focused.
|
|
* If not focused, will check if the button element is focused.
|
|
*/
|
|
private checkOverlayListFocused() {
|
|
if (globalThis.document.hasFocus()) {
|
|
return;
|
|
}
|
|
|
|
this.postMessageToParent({ command: "checkAutofillOverlayButtonFocused" });
|
|
}
|
|
|
|
/**
|
|
* Focuses the overlay list iframe. The element that receives focus is
|
|
* determined by the presence of the unlock button, new item button, or
|
|
* the first cipher button.
|
|
*/
|
|
private focusOverlayList() {
|
|
const unlockButtonElement = this.overlayListContainer.querySelector(
|
|
"#unlock-button",
|
|
) as HTMLElement;
|
|
if (unlockButtonElement) {
|
|
unlockButtonElement.focus();
|
|
return;
|
|
}
|
|
|
|
const newItemButtonElement = this.overlayListContainer.querySelector(
|
|
"#new-item-button",
|
|
) as HTMLElement;
|
|
if (newItemButtonElement) {
|
|
newItemButtonElement.focus();
|
|
return;
|
|
}
|
|
|
|
const firstCipherElement = this.overlayListContainer.querySelector(
|
|
".fill-cipher-button",
|
|
) as HTMLElement;
|
|
firstCipherElement?.focus();
|
|
}
|
|
|
|
/**
|
|
* Sets up the global listeners for the overlay list iframe.
|
|
*/
|
|
private setupOverlayListGlobalListeners() {
|
|
this.setupGlobalListeners(this.overlayListWindowMessageHandlers);
|
|
|
|
this.resizeObserver = new ResizeObserver(this.handleResizeObserver);
|
|
}
|
|
|
|
/**
|
|
* Handles the resize observer event. Facilitates updating the height of the
|
|
* overlay list iframe when the height of the list changes.
|
|
*
|
|
* @param entries - The resize observer entries.
|
|
*/
|
|
private handleResizeObserver = (entries: ResizeObserverEntry[]) => {
|
|
for (let entryIndex = 0; entryIndex < entries.length; entryIndex++) {
|
|
const entry = entries[entryIndex];
|
|
if (entry.target !== this.overlayListContainer) {
|
|
continue;
|
|
}
|
|
|
|
const { height } = entry.contentRect;
|
|
this.postMessageToParent({
|
|
command: "updateAutofillOverlayListHeight",
|
|
styles: { height: `${height}px` },
|
|
});
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Establishes a memoized event handler for a given event.
|
|
*
|
|
* @param eventHandler - The event handler to memoize.
|
|
* @param memoIndex - The memo index to use for the event handler.
|
|
*/
|
|
private useEventHandlersMemo = (eventHandler: EventListener, memoIndex: string) => {
|
|
return this.eventHandlersMemo[memoIndex] || (this.eventHandlersMemo[memoIndex] = eventHandler);
|
|
};
|
|
|
|
/**
|
|
* Focuses the next list item in the overlay list. If the current list item is the last
|
|
* item in the list, the first item is focused.
|
|
*
|
|
* @param currentListItem - The current list item.
|
|
*/
|
|
private focusNextListItem(currentListItem: HTMLElement) {
|
|
const nextListItem = currentListItem.nextSibling as HTMLElement;
|
|
const nextSibling = nextListItem?.querySelector(".fill-cipher-button") as HTMLElement;
|
|
if (nextSibling) {
|
|
nextSibling.focus();
|
|
return;
|
|
}
|
|
|
|
const firstListItem = currentListItem.parentElement?.firstChild as HTMLElement;
|
|
const firstSibling = firstListItem?.querySelector(".fill-cipher-button") as HTMLElement;
|
|
firstSibling?.focus();
|
|
}
|
|
|
|
/**
|
|
* Focuses the previous list item in the overlay list. If the current list item is the first
|
|
* item in the list, the last item is focused.
|
|
*
|
|
* @param currentListItem - The current list item.
|
|
*/
|
|
private focusPreviousListItem(currentListItem: HTMLElement) {
|
|
const previousListItem = currentListItem.previousSibling as HTMLElement;
|
|
const previousSibling = previousListItem?.querySelector(".fill-cipher-button") as HTMLElement;
|
|
if (previousSibling) {
|
|
previousSibling.focus();
|
|
return;
|
|
}
|
|
|
|
const lastListItem = currentListItem.parentElement?.lastChild as HTMLElement;
|
|
const lastSibling = lastListItem?.querySelector(".fill-cipher-button") as HTMLElement;
|
|
lastSibling?.focus();
|
|
}
|
|
|
|
/**
|
|
* Focuses the view cipher button relative to the current fill cipher button.
|
|
*
|
|
* @param currentListItem - The current list item.
|
|
* @param currentButtonElement - The current button element.
|
|
*/
|
|
private focusViewCipherButton(currentListItem: HTMLElement, currentButtonElement: HTMLElement) {
|
|
const cipherContainer = currentListItem.querySelector(".cipher-container") as HTMLElement;
|
|
cipherContainer.classList.add("remove-outline");
|
|
|
|
const nextSibling = currentButtonElement.nextElementSibling as HTMLElement;
|
|
nextSibling?.focus();
|
|
}
|
|
}
|
|
|
|
export default AutofillOverlayList;
|