diff options
Diffstat (limited to 'toolkit/components/satchel/megalist/content')
6 files changed, 472 insertions, 74 deletions
diff --git a/toolkit/components/satchel/megalist/content/Dialog.mjs b/toolkit/components/satchel/megalist/content/Dialog.mjs new file mode 100644 index 0000000000..f2eca6376c --- /dev/null +++ b/toolkit/components/satchel/megalist/content/Dialog.mjs @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +const GENERIC_DIALOG_TEMPLATE = document.querySelector("#dialog-template"); + +const DIALOGS = { + "remove-login": { + template: "#remove-login-dialog-template", + }, + "export-logins": { + template: "#export-logins-dialog-template", + }, + "remove-logins": { + template: "#remove-logins-dialog-template", + callback: dialog => { + const primaryButton = dialog.querySelector("button.primary"); + const checkbox = dialog.querySelector(".confirm-checkbox"); + const toggleButton = () => (primaryButton.disabled = !checkbox.checked); + checkbox.addEventListener("change", toggleButton); + toggleButton(); + }, + }, + "import-logins": { + template: "#import-logins-dialog-template", + }, + "import-error": { + template: "#import-error-dialog-template", + }, +}; + +/** + * Setup dismiss and command handling logic for the dialog overlay. + * + * @param {Element} overlay - The overlay element containing the dialog + * @param {Function} messageHandler - Function to send message back to view model. + */ +const setupControls = (overlay, messageHandler) => { + const dialog = overlay.querySelector(".dialog-container"); + const commandButtons = dialog.querySelectorAll("[data-command]"); + for (const commandButton of commandButtons) { + const commandId = commandButton.dataset.command; + commandButton.addEventListener("click", () => messageHandler(commandId)); + } + + dialog.querySelectorAll("[close-dialog]").forEach(element => { + element.addEventListener("click", cancelDialog, { once: true }); + }); + + document.addEventListener("keyup", function handleKeyUp(ev) { + if (ev.key === "Escape") { + cancelDialog(); + document.removeEventListener("keyup", handleKeyUp); + } + }); + + document.addEventListener("click", function handleClickOutside(ev) { + if (!dialog.contains(ev.target)) { + cancelDialog(); + document.removeEventListener("click", handleClickOutside); + } + }); + dialog.querySelector("[autofocus]")?.focus(); +}; + +/** + * Add data-l10n-args to elements with localizable attribute + * + * @param {Element} dialog - The dialog element. + * @param {Array<object>} l10nArgs - List of localization arguments. + */ +const populateL10nArgs = (dialog, l10nArgs) => { + const localizableElements = dialog.querySelectorAll("[localizable]"); + for (const [index, localizableElement] of localizableElements.entries()) { + localizableElement.dataset.l10nArgs = JSON.stringify(l10nArgs[index]) ?? ""; + } +}; + +/** + * Remove the currently displayed dialog overlay from the DOM. + */ +export const cancelDialog = () => + document.querySelector(".dialog-overlay")?.remove(); + +/** + * Create a new dialog overlay and populate it using the specified template and data. + * + * @param {object} dialogData - Data required to populate the dialog, includes template and localization args. + * @param {Function} messageHandler - Function to send message back to view model. + */ +export const createDialog = (dialogData, messageHandler) => { + const templateData = DIALOGS[dialogData?.id]; + + const genericTemplateClone = document.importNode( + GENERIC_DIALOG_TEMPLATE.content, + true + ); + + const overlay = genericTemplateClone.querySelector(".dialog-overlay"); + const dialog = genericTemplateClone.querySelector(".dialog-container"); + + const overrideTemplate = document.querySelector(templateData.template); + const overrideTemplateClone = document.importNode( + overrideTemplate.content, + true + ); + + genericTemplateClone + .querySelector(".dialog-wrapper") + .appendChild(overrideTemplateClone); + + populateL10nArgs(genericTemplateClone, dialogData.l10nArgs); + setupControls(overlay, messageHandler); + document.body.appendChild(genericTemplateClone); + templateData?.callback?.(dialog, messageHandler); +}; diff --git a/toolkit/components/satchel/megalist/content/MegalistView.mjs b/toolkit/components/satchel/megalist/content/MegalistView.mjs index 44a0198692..feec2409f8 100644 --- a/toolkit/components/satchel/megalist/content/MegalistView.mjs +++ b/toolkit/components/satchel/megalist/content/MegalistView.mjs @@ -4,13 +4,14 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import { + createDialog, + cancelDialog, +} from "chrome://global/content/megalist/Dialog.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/megalist/VirtualizedList.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://global/content/megalist/search-input.mjs"; - /** * Map with limit on how many entries it can have. * When over limit entries are added, oldest one are removed. @@ -77,6 +78,7 @@ export class MegalistView extends MozLitElement { super(); this.selectedIndex = 0; this.searchText = ""; + this.layout = null; window.addEventListener("MessageFromViewModel", ev => this.#onMessageFromViewModel(ev) @@ -88,6 +90,7 @@ export class MegalistView extends MozLitElement { listLength: { type: Number }, selectedIndex: { type: Number }, searchText: { type: String }, + layout: { type: Object }, }; } @@ -112,6 +115,10 @@ export class MegalistView extends MozLitElement { #templates = {}; + static queries = { + searchInput: ".search", + }; + connectedCallback() { super.connectedCallback(); this.ownerDocument.addEventListener("keydown", e => this.#handleKeydown(e)); @@ -296,6 +303,16 @@ export class MegalistView extends MozLitElement { this.requestUpdate(); } + receiveSetLayout({ layout }) { + if (layout) { + createDialog(layout, commandId => + this.#messageToViewModel("Command", { commandId }) + ); + } else { + cancelDialog(); + } + } + #handleInputChange(e) { const searchText = e.target.value; this.#messageToViewModel("UpdateFilter", { searchText }); @@ -333,6 +350,15 @@ export class MegalistView extends MozLitElement { } #handleClick(e) { + const elementWithCommand = e.composedTarget.closest("[data-command]"); + if (elementWithCommand) { + const commandId = elementWithCommand.dataset.command; + if (commandId) { + this.#messageToViewModel("Command", { commandId }); + return; + } + } + const lineElement = e.composedTarget.closest(".line"); if (!lineElement) { return; @@ -360,6 +386,12 @@ export class MegalistView extends MozLitElement { const popup = this.ownerDocument.createElement("div"); popup.className = "menuPopup"; + + let closeMenu = () => { + popup.remove(); + this.searchInput.focus(); + }; + popup.addEventListener( "keydown", e => { @@ -385,7 +417,7 @@ export class MegalistView extends MozLitElement { switch (e.code) { case "Escape": - popup.remove(); + closeMenu(); break; case "Tab": if (e.shiftKey) { @@ -416,9 +448,7 @@ export class MegalistView extends MozLitElement { e.composedTarget?.closest(".menuPopup") != e.relatedTarget?.closest(".menuPopup") ) { - // TODO: this triggers on macOS before "click" event. Due to this, - // we are not receiving the command. - popup.remove(); + closeMenu(); } }, { capture: true } @@ -433,7 +463,7 @@ export class MegalistView extends MozLitElement { } const menuItem = this.ownerDocument.createElement("button"); - menuItem.textContent = command.label; + menuItem.setAttribute("data-l10n-id", command.label); menuItem.addEventListener("click", e => { this.#messageToViewModel("Command", { snapshotId, @@ -449,26 +479,50 @@ export class MegalistView extends MozLitElement { popup.querySelector("button")?.focus(); } + /** + * Renders data-source specific UI that should be displayed before the + * virtualized list. This is determined by the "SetLayout" message provided + * by the View Model. Defaults to displaying the search input. + */ + renderBeforeList() { + return html` + <input + class="search" + type="search" + data-l10n-id="filter-placeholder" + .value=${this.searchText} + @input=${e => this.#handleInputChange(e)} + /> + `; + } + + renderList() { + if (this.layout) { + return null; + } + + return html` <virtualized-list + .lineCount=${this.listLength} + .lineHeight=${MegalistView.LINE_HEIGHT} + .selectedIndex=${this.selectedIndex} + .createLineElement=${index => this.createLineElement(index)} + @click=${e => this.#handleClick(e)} + > + </virtualized-list>`; + } + + renderAfterList() {} + render() { return html` <link rel="stylesheet" href="chrome://global/content/megalist/megalist.css" /> - <div class="container"> - <search-input - .value=${this.searchText} - .change=${e => this.#handleInputChange(e)} - > - </search-input> - <virtualized-list - .lineCount=${this.listLength} - .lineHeight=${MegalistView.LINE_HEIGHT} - .selectedIndex=${this.selectedIndex} - .createLineElement=${index => this.createLineElement(index)} - @click=${e => this.#handleClick(e)} - > - </virtualized-list> + <div @click=${this.#handleClick} class="container"> + <div class="beforeList">${this.renderBeforeList()}</div> + ${this.renderList()} + <div class="afterList">${this.renderAfterList()}</div> </div> `; } diff --git a/toolkit/components/satchel/megalist/content/megalist.css b/toolkit/components/satchel/megalist/content/megalist.css index b442a7b60d..3f8bb9de2c 100644 --- a/toolkit/components/satchel/megalist/content/megalist.css +++ b/toolkit/components/satchel/megalist/content/megalist.css @@ -8,18 +8,27 @@ display: flex; flex-direction: column; justify-content: center; - max-height: 100vh; + height: 100vh; - > search-input { + > .beforeList { margin: 20px; + + .search { + padding: 8px; + border-radius: 4px; + border: 1px solid var(--in-content-border-color); + box-sizing: border-box; + width: 100%; + } } } virtualized-list { position: relative; overflow: auto; - margin: 20px; - + margin-block: 20px; + padding-inline: 20px; + flex-grow: 1; .lines-container { padding-inline-start: unset; } @@ -29,7 +38,7 @@ virtualized-list { display: flex; align-items: stretch; position: absolute; - width: 100%; + width: calc(100% - 40px); user-select: none; box-sizing: border-box; height: 64px; @@ -93,11 +102,19 @@ virtualized-list { > .content { flex-grow: 1; + &:not(.section) { + display: grid; + grid-template-rows: max-content 1fr; + grid-template-columns: max-content; + grid-column-gap: 8px; + padding-inline-start: 8px; + padding-block-start: 4px; + } + > div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - padding-inline-start: 10px; &:last-child { padding-block-end: 10px; @@ -115,6 +132,8 @@ virtualized-list { > .label { color: var(--text-color-deemphasized); padding-block: 2px 4px; + grid-row: 1; + align-content: end; } > .value { @@ -125,7 +144,7 @@ virtualized-list { fill: currentColor; width: auto; height: 16px; - margin-inline: 4px; + margin-inline-end: 4px; vertical-align: text-bottom; } @@ -139,12 +158,14 @@ virtualized-list { } > .stickers { - text-align: end; - margin-block-start: 2px; + grid-row: 1; + align-content: start; > span { - padding: 2px; + padding: 4px; margin-inline-end: 2px; + border-radius: 24px; + font-size: xx-small; } /* Hard-coded colors will be addressed in FXCM-1013 */ @@ -159,6 +180,12 @@ virtualized-list { border: 1px solid maroon; color: whitesmoke; } + + > span.error { + background-color: orange; + border: 1px solid orangered; + color: black; + } } &.section { @@ -199,10 +226,46 @@ virtualized-list { } } -.search { - padding: 8px; - border-radius: 4px; - border: 1px solid var(--in-content-border-color); - box-sizing: border-box; +/* Dialog styles */ +.dialog-overlay { + display: flex; + justify-content: center; + align-items: center; + padding: 16px; + position: fixed; + top: 0; + left: 0; width: 100%; + height: 100%; + z-index: 1; + background-color: rgba(0, 0, 0, 0.5); + box-sizing: border-box; + /* TODO: probably want to remove this later ? */ + backdrop-filter: blur(6px); +} + +.dialog-container { + display: grid; + padding: 16px 32px; + color: var(--in-content-text-color); + background-color: var(--in-content-box-background); + border: 1px solid var(--in-content-border-color); + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1); +} + +.dialog-title { + margin: 0; +} + +.dismiss-button { + justify-self: end; +} + +.dialog-content { + margin-block-start: 16px; + margin-block-end: 16px; + + .checkbox-text { + margin-block-start: 8px; + } } diff --git a/toolkit/components/satchel/megalist/content/megalist.ftl b/toolkit/components/satchel/megalist/content/megalist.ftl index 69d085a7c5..4089477add 100644 --- a/toolkit/components/satchel/megalist/content/megalist.ftl +++ b/toolkit/components/satchel/megalist/content/megalist.ftl @@ -23,6 +23,7 @@ command-cancel = Cancel passwords-section-label = Passwords passwords-disabled = Passwords are disabled +passwords-dismiss-breach-alert-command = Dismiss breach alert passwords-command-create = Add Password passwords-command-import = Import from a File… passwords-command-export = Export Passwords… @@ -65,6 +66,33 @@ passwords-filtered-count = *[other] { $count } of { $total } passwords } +# Confirm the removal of all saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-title = + { $total -> + [one] Remove { $total } password? + *[other] Remove all { $total } passwords? + } + +# Checkbox label to confirm the removal of saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-confirm = + { $total -> + [1] Yes, remove password + *[other] Yes, remove passwords + } + +# Button label to confirm removal of saved passwords +passwords-remove-all-confirm-button = Confirm + +# Message to confirm the removal of saved passwords +# $total (number) - Total number of passwords +passwords-remove-all-message = + { $total -> + [1] This will remove your saved password and any breach alerts. You cannot undo this action. + *[other] This will remove your saved passwords and any breach alerts. You cannot undo this action. + } + passwords-origin-label = Website address passwords-username-label = Username passwords-password-label = Password diff --git a/toolkit/components/satchel/megalist/content/megalist.html b/toolkit/components/satchel/megalist/content/megalist.html index 6ff3f089fc..9d15587033 100644 --- a/toolkit/components/satchel/megalist/content/megalist.html +++ b/toolkit/components/satchel/megalist/content/megalist.html @@ -15,7 +15,12 @@ src="chrome://global/content/megalist/MegalistView.mjs" ></script> <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> + <link + rel="stylesheet" + href="chrome://global/content/megalist/megalist.css" + /> <link rel="localization" href="preview/megalist.ftl" /> + <link rel="localization" href="browser/aboutLogins.ftl" /> </head> <body> @@ -56,11 +61,11 @@ <template id="lineTemplate"> <div class="content"> <div class="label"></div> + <div class="stickers"></div> <div class="value"> <img class="icon" /> <span></span> </div> - <div class="stickers"></div> </div> </template> @@ -74,5 +79,173 @@ <div class="stickers"></div> </div> </template> + + <template id="dialog-template"> + <div class="dialog-overlay"> + <div class="dialog-container"> + <moz-button + data-l10n-id="confirmation-dialog-dismiss-button" + iconSrc="chrome://global/skin/icons/close.svg" + size="small" + type="icon ghost" + class="dismiss-button" + close-dialog + > + </moz-button> + <div class="dialog-wrapper"></div> + </div> + </div> + </template> + + <template id="remove-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-remove-all-sync-dialog-title2" + localizable + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p> + <label> + <input type="checkbox" class="confirm-checkbox checkbox" autofocus /> + <span + class="checkbox-text" + data-l10n-id="about-logins-confirm-remove-all-dialog-checkbox-label2" + ></span> + </label> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-remove-all-dialog-confirm-button-label" + data-command="LoginDataSource.confirmRemoveAll" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + + <template id="remove-login-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-delete-dialog-title" + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-delete-dialog-message"></p> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-remove-dialog-confirm-button" + data-command="LoginDataSource.confirmRemoveLogin" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + <template id="export-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-confirm-export-dialog-title2" + ></h2> + <div class="dialog-content" slot="dialog-content"> + <p data-l10n-id="about-logins-confirm-export-dialog-message2"></p> + </div> + <moz-button-group> + <button + class="primary danger-button" + data-l10n-id="about-logins-confirm-export-dialog-confirm-button2" + data-command="LoginDataSource.confirmExportLogins" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> + + <template id="import-logins-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-import-dialog-title" + ></h2> + <div class="dialog-content"> + <div data-l10n-id="about-logins-import-dialog-items-added2" localizable> + <span></span> + <span data-l10n-name="count"></span> + </div> + <div + data-l10n-id="about-logins-import-dialog-items-modified2" + localizable + > + <span></span> + <span data-l10n-name="count"></span> + </div> + <div + data-l10n-id="about-logins-import-dialog-items-no-change2" + data-l10n-name="no-change" + localizable + > + <span></span> + <span data-l10n-name="count"></span> + <span data-l10n-name="meta"></span> + </div> + <div data-l10n-id="about-logins-import-dialog-items-error" localizable> + <span></span> + <span data-l10n-name="count"></span> + <span data-l10n-name="meta"></span> + </div> + <a + class="open-detailed-report" + href="about:loginsimportreport" + target="_blank" + data-l10n-id="about-logins-alert-import-message" + ></a> + </div> + <button + class="primary" + data-l10n-id="about-logins-import-dialog-done" + close-dialog + ></button> + </template> + + <template id="import-error-dialog-template"> + <h2 + class="dialog-title" + data-l10n-id="about-logins-import-dialog-error-title" + ></h2> + <div class="dialog-content"> + <p + data-l10n-id="about-logins-import-dialog-error-file-format-title" + ></p> + <p + data-l10n-id="about-logins-import-dialog-error-file-format-description" + ></p> + <p + data-l10n-id="about-logins-import-dialog-error-no-logins-imported" + ></p> + <a + class="error-learn-more-link" + href="https://support.mozilla.org/kb/import-login-data-file" + data-l10n-id="about-logins-import-dialog-error-learn-more" + target="_blank" + rel="noreferrer" + ></a> + </div> + <moz-button-group> + <button + class="primary" + data-l10n-id="about-logins-import-dialog-error-try-import-again" + data-command="LoginDataSource.confirmRetryImport" + ></button> + <button + close-dialog + data-l10n-id="confirmation-dialog-cancel-button" + ></button> + </moz-button-group> + </template> </body> </html> diff --git a/toolkit/components/satchel/megalist/content/search-input.mjs b/toolkit/components/satchel/megalist/content/search-input.mjs deleted file mode 100644 index e30d13ef2a..0000000000 --- a/toolkit/components/satchel/megalist/content/search-input.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { html } from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; - -export default class SearchInput extends MozLitElement { - static get properties() { - return { - items: { type: Array }, - change: { type: Function }, - value: { type: String }, - }; - } - - render() { - return html` <link - rel="stylesheet" - href="chrome://global/content/megalist/megalist.css" - /> - <link - rel="stylesheet" - href="chrome://global/skin/in-content/common.css" - /> - <input - class="search" - type="search" - data-l10n-id="filter-placeholder" - @input=${this.change} - .value=${this.value} - />`; - } -} - -customElements.define("search-input", SearchInput); |