Merge mozilla-central to inbound. a=merge CLOSED TREE

This commit is contained in:
Oana Pop Rus 2019-07-04 06:38:21 +03:00
commit 2ad74ef9f7
469 changed files with 5909 additions and 5112 deletions

View File

@ -118,7 +118,7 @@ media/gmp-clearkey/0.1/openaes/.*
media/kiss_fft/.*
media/libaom/.*
media/libcubeb/.*
media/libdav1d/version.h
media/libdav1d/.*
media/libjpeg/.*
media/libmkv/.*
media/libnestegg/.*

View File

@ -176,6 +176,9 @@ jobs:
mozilla-esr60:
- {weekday: 'Monday', hour: 10, minute: 0}
- {weekday: 'Thursday', hour: 10, minute: 0}
mozilla-esr68:
- {weekday: 'Monday', hour: 10, minute: 0}
- {weekday: 'Thursday', hour: 10, minute: 0}
- name: pipfile-update
job:

View File

@ -39,7 +39,13 @@
// change list style type
var list = getNode("list");
list.setAttribute("style", "list-style-type: disc;");
getComputedStyle(list, "").color; // make style processing sync
// Flush both the style change and the resulting layout change.
// Flushing style on its own is not sufficient, because that can
// leave frames marked with NS_FRAME_IS_DIRTY, which will cause
// nsTextFrame::GetRenderedText to report the text of a text
// frame is empty.
list.offsetWidth; // flush layout (which also flushes style)
testName("li_start", kDiscBulletText + "list start");
testName("li_end", kDiscBulletText + "list end");

View File

@ -125,6 +125,7 @@ class ContextMenuChild extends JSWindowActorChild {
}
break;
case "pictureinpicture":
Services.telemetry.keyedScalarAdd("pictureinpicture.opened_method", "contextmenu", 1);
let event = new this.contentWindow.CustomEvent("MozTogglePictureInPicture", {
bubbles: true,
}, this.contentWindow);

View File

@ -971,19 +971,25 @@ var ContentBlocking = {
this.identityPopupMultiView.goBack();
},
submitBreakageReport() {
onSubmitBreakageReportClicked() {
this.identityPopup.hidePopup();
let comments = document.getElementById(
"identity-popup-breakageReportView-collection-comments");
this.submitBreakageReport(this.reportURI, comments);
},
submitBreakageReport(uri, commentsTextarea) {
let reportEndpoint = Services.prefs.getStringPref(this.PREF_REPORT_BREAKAGE_URL);
if (!reportEndpoint) {
return;
}
let formData = new FormData();
formData.set("title", this.reportURI.host);
formData.set("title", uri.host);
// Leave the ? at the end of the URL to signify that this URL had its query stripped.
let urlWithoutQuery = this.reportURI.asciiSpec.replace(this.reportURI.query, "");
let urlWithoutQuery = uri.asciiSpec.replace(uri.query, "");
let body = `Full URL: ${urlWithoutQuery}\n`;
body += `userAgent: ${navigator.userAgent}\n`;
@ -995,12 +1001,12 @@ var ContentBlocking = {
body += `network.http.referer.defaultPolicy.pbmode: ${Services.prefs.getIntPref("network.http.referer.defaultPolicy.pbmode")}\n`;
body += `${ThirdPartyCookies.PREF_ENABLED}: ${Services.prefs.getIntPref(ThirdPartyCookies.PREF_ENABLED)}\n`;
body += `network.cookie.lifetimePolicy: ${Services.prefs.getIntPref("network.cookie.lifetimePolicy")}\n`;
body += `privacy.annotate_channels.strict_list.enabled: ${Services.prefs.getBoolPref("privacy.annotate_channels.strict_list.enabled")}\n`;
body += `privacy.restrict3rdpartystorage.expiration: ${Services.prefs.getIntPref("privacy.restrict3rdpartystorage.expiration")}\n`;
body += `${Fingerprinting.PREF_ENABLED}: ${Services.prefs.getBoolPref(Fingerprinting.PREF_ENABLED)}\n`;
body += `${Cryptomining.PREF_ENABLED}: ${Services.prefs.getBoolPref(Cryptomining.PREF_ENABLED)}\n`;
let comments = document.getElementById("identity-popup-breakageReportView-collection-comments");
body += "\n**Comments**\n" + comments.value;
body += "\n**Comments**\n" + commentsTextarea.value;
formData.set("body", body);
@ -1024,7 +1030,7 @@ var ContentBlocking = {
Cu.reportError(`Content Blocking report to ${reportEndpoint} failed with status ${response.status}`);
} else {
// Clear the textarea value when the report is submitted
comments.value = "";
commentsTextarea.value = "";
}
}).catch(Cu.reportError);
},

View File

@ -17,6 +17,11 @@ var gProtectionsHandler = {
delete this._protectionsIconBox;
return this._protectionsIconBox = document.getElementById("tracking-protection-icon-animatable-box");
},
get _protectionsPopupMultiView() {
delete this._protectionsPopupMultiView;
return this._protectionsPopupMultiView =
document.getElementById("protections-popup-multiView");
},
get _protectionsPopupMainView() {
delete this._protectionsPopupMainView;
return this._protectionsPopupMainView =
@ -52,6 +57,21 @@ var gProtectionsHandler = {
return this._protectionPopupTrackersCounterDescription =
document.getElementById("protections-popup-trackers-blocked-counter-description");
},
get _protectionsPopupSiteNotWorkingTPSwitch() {
delete this._protectionsPopupSiteNotWorkingTPSwitch;
return this._protectionsPopupSiteNotWorkingTPSwitch =
document.getElementById("protections-popup-siteNotWorking-tp-switch");
},
get _protectionsPopupSendReportLearnMore() {
delete this._protectionsPopupSendReportLearnMore;
return this._protectionsPopupSendReportLearnMore =
document.getElementById("protections-popup-sendReportView-learn-more");
},
get _protectionsPopupSendReportURL() {
delete this._protectionsPopupSendReportURL;
return this._protectionsPopupSendReportURL =
document.getElementById("protections-popup-sendReportView-collection-url");
},
get _protectionsPopupToastTimeout() {
delete this._protectionsPopupToastTimeout;
XPCOMUtils.defineLazyPreferenceGetter(this, "_protectionsPopupToastTimeout",
@ -128,10 +148,6 @@ var gProtectionsHandler = {
},
refreshProtectionsPopup() {
// Refresh the state of the TP toggle switch.
this._protectionsPopupTPSwitch.toggleAttribute("enabled",
!this._protectionsPopup.hasAttribute("hasException"));
let host = gIdentityHandler.getHostForDisplay();
// Push the appropriate strings out to the UI.
@ -142,7 +158,10 @@ var gProtectionsHandler = {
let currentlyEnabled =
!this._protectionsPopup.hasAttribute("hasException");
this._protectionsPopupTPSwitch.toggleAttribute("enabled", currentlyEnabled);
for (let tpSwitch of [this._protectionsPopupTPSwitch,
this._protectionsPopupSiteNotWorkingTPSwitch]) {
tpSwitch.toggleAttribute("enabled", currentlyEnabled);
}
// Display the breakage link according to the current enable state.
// The display state of the breakage link will be fixed once the protections
@ -171,7 +190,10 @@ var gProtectionsHandler = {
// styling after toggling the TP switch.
let newExceptionState =
this._protectionsPopup.toggleAttribute("hasException");
this._protectionsPopupTPSwitch.toggleAttribute("enabled", !newExceptionState);
for (let tpSwitch of [this._protectionsPopupTPSwitch,
this._protectionsPopupSiteNotWorkingTPSwitch]) {
tpSwitch.toggleAttribute("enabled", !newExceptionState);
}
// Indicating that we need to show a toast after refreshing the page.
// And caching the current URI and window ID in order to only show the mini
@ -248,4 +270,27 @@ var gProtectionsHandler = {
triggerEvent: event,
}).catch(Cu.reportError);
},
showSiteNotWorkingView() {
this._protectionsPopupMultiView.showSubView("protections-popup-siteNotWorkingView");
},
showSendReportView() {
// Save this URI to make sure that the user really only submits the location
// they see in the report breakage dialog.
this.reportURI = gBrowser.currentURI;
let urlWithoutQuery = this.reportURI.asciiSpec.replace("?" + this.reportURI.query, "");
this._protectionsPopupSendReportURL.value = urlWithoutQuery;
this._protectionsPopupMultiView.showSubView("protections-popup-sendReportView");
},
onSendReportClicked() {
this._protectionsPopup.hidePopup();
let comments = document.getElementById(
"protections-popup-sendReportView-collection-comments");
ContentBlocking.submitBreakageReport(this.reportURI, comments);
},
};
let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
gProtectionsHandler._protectionsPopupSendReportLearnMore.href = baseURL + "blocking-breakage";

View File

@ -40,6 +40,22 @@ add_task(async function testToggleSwitch() {
BrowserTestUtils.removeTab(tab);
});
add_task(async function testSiteNotWorking() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com");
await openProtectionsPanel();
let viewShownPromise = BrowserTestUtils.waitForEvent(
gProtectionsHandler._protectionsPopupMultiView, "ViewShown");
document.getElementById("protections-popup-tp-switch-breakage-link").click();
let event = await viewShownPromise;
is(event.originalTarget.id, "protections-popup-siteNotWorkingView", "Site Not Working? view should be shown");
viewShownPromise = BrowserTestUtils.waitForEvent(
gProtectionsHandler._protectionsPopupMultiView, "ViewShown");
document.getElementById("protections-popup-siteNotWorkingView-sendReport").click();
event = await viewShownPromise;
is(event.originalTarget.id, "protections-popup-sendReportView", "Send Report view should be shown");
BrowserTestUtils.removeTab(tab);
});
/**
* A test for the protection settings button.
*/

View File

@ -265,6 +265,7 @@ async function testReportBreakage(url, tags) {
"network.http.referer.defaultPolicy.pbmode",
"network.cookie.cookieBehavior",
"network.cookie.lifetimePolicy",
"privacy.annotate_channels.strict_list.enabled",
"privacy.restrict3rdpartystorage.expiration",
"privacy.trackingprotection.fingerprinting.enabled",
"privacy.trackingprotection.cryptomining.enabled",

View File

@ -220,12 +220,10 @@ var AboutLoginsParent = {
},
async showMasterPasswordLoginNotifications() {
if (!this._l10n) {
this._l10n = new Localization(["browser/aboutLogins.ftl"]);
}
let messageString = await this._l10n.formatValue("master-password-notification-message");
for (let subscriber of this._subscriberIterator()) {
let MozXULElement = subscriber.ownerGlobal.MozXULElement;
MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
// If there's already an existing notification bar, don't do anything.
let {gBrowser} = subscriber.ownerGlobal;
let browser = subscriber;
@ -238,17 +236,20 @@ var AboutLoginsParent = {
// Configure the notification bar
let priority = notificationBox.PRIORITY_WARNING_MEDIUM;
let iconURL = "chrome://browser/skin/login.svg";
let reloadLabel = await this._l10n.formatValue("master-password-reload-button-label");
let reloadKey = await this._l10n.formatValue("master-password-reload-button-accesskey");
let doc = subscriber.ownerDocument;
let messageFragment = doc.createDocumentFragment();
let message = doc.createElement("span");
doc.l10n.setAttributes(message, "master-password-notification-message");
messageFragment.appendChild(message);
let buttons = [{
label: reloadLabel,
accessKey: reloadKey,
"l10n-id": "master-password-reload-button",
popup: null,
callback() { browser.reload(); },
}];
notification = notificationBox.appendNotification(messageString, MASTER_PASSWORD_NOTIFICATION_ID,
notification = notificationBox.appendNotification(messageFragment, MASTER_PASSWORD_NOTIFICATION_ID,
iconURL, priority, buttons);
}
},

View File

@ -6,67 +6,76 @@
### being translated as the feature is still in heavy development
### and strings are likely to change often.
### Fluent isn't translating elements in the shadow DOM so the translated strings
### need to be applied to the composed node where they can be moved to the proper
### descendant after translation.
about-logins-page-title = Logins & Passwords
create-login-button = New Login
login-filter =
.placeholder = Search Logins
create-login-button = New Login
## The ⋯ menu that is in the top corner of the page
menu =
.title = Open menu
menu-menuitem-faq = Frequently Asked Questions
menu-menuitem-feedback = Leave Feedback
menu-menuitem-import = Import Passwords…
menu-menuitem-preferences =
{ PLATFORM() ->
[windows] Options
*[other] Preferences
}
## Login List
login-list =
.aria-label = Logins matching search query
.count =
{ $count ->
[one] { $count } login
*[other] { $count } logins
}
.last-changed-option = Last Changed
.last-used-option = Last Used
.missing-username = (no username)
.name-option = Name
.new-login-subtitle = Enter your login credentials
.new-login-title = New Login
.sort-label-text = Sort by:
login-list-count =
{ $count ->
[one] { $count } login
*[other] { $count } logins
}
login-list-last-changed-option = Last Changed
login-list-last-used-option = Last Used
login-list-name-option = Name
login-list-sort-label-text = Sort by:
login-list-item-title-new-login = New Login
login-list-item-subtitle-new-login = Enter your login credentials
login-list-item-subtitle-missing-username = (no username)
login-item =
.cancel-button = Cancel
.copied-password-button = ✓ Copied!
.copied-username-button = ✓ Copied!
.copy-password-button = Copy
.copy-username-button = Copy
.delete-button = Delete
.edit-button = Edit
.new-login-title = Create New Login
.open-site-button = Launch
.origin-label = Website Address
.origin-placeholder = https://www.example.com
.password-hide-title = Hide password
.password-label = Password
.password-show-title = Show password
.save-changes-button = Save Changes
.time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
.time-changed = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
.time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
.username-label = Username
.username-placeholder = name@example.com
## Login
login-item-new-login-title = Create New Login
login-item-edit-button = Edit
login-item-delete-button = Delete
login-item-origin-label = Website Address
login-item-origin =
.placeholder = https://www.example.com
login-item-open-site-button = Launch
login-item-username-label = Username
login-item-username =
.placeholder = name@example.com
login-item-copied-username-button-text = ✔ Copied!
login-item-copy-username-button-text = Copy
login-item-password-label = Password
login-item-password-reveal-checkbox-show =
.title = Show password
login-item-password-reveal-checkbox-hide =
.title = Hide password
login-item-copied-password-button-text = ✔ Copied!
login-item-copy-password-button-text = Copy
login-item-save-changes-button = Save Changes
login-item-cancel-button = Cancel
login-item-time-changed = Last modified: { DATETIME($timeChanged, day: "numeric", month: "long", year: "numeric") }
login-item-time-created = Created: { DATETIME($timeCreated, day: "numeric", month: "long", year: "numeric") }
login-item-time-used = Last used: { DATETIME($timeUsed, day: "numeric", month: "long", year: "numeric") }
## Master Password notification
master-password-notification-message = Please enter your master password to view saved logins & passwords
# TODO: Not sure how to use formatValue with these as attributes on a single ID
master-password-reload-button-label = Log in
# TODO: Not sure how to use formatValue with these as attributes on a single ID
master-password-reload-button-accesskey = L
master-password-reload-button =
.label = Log in
.accesskey = L
menu-button =
.button-title = Open menu
.menuitem-faq = Frequently Asked Questions
.menuitem-feedback = Leave Feedback
.menuitem-import = Import Passwords…
.menuitem-preferences =
{ PLATFORM() ->
[windows] Options
*[other] Preferences
}
confirm-delete-dialog-title = Confirm Deletion
confirm-delete-dialog-message = Are you sure you want to delete this login?
confirm-delete-dialog-dismiss-button =
.title = Cancel
confirm-delete-dialog-cancel-button = Cancel
confirm-delete-dialog-confirm-button = Delete login

View File

@ -9,7 +9,7 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; object-src 'none'; script-src resource: chrome:; img-src data: blob:;"/>
<title data-l10n-id="about-logins-page-title"></title>
<link rel="localization" href="browser/aboutLogins.ftl">
<script type="module" src="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/login-filter.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/login-item.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/login-list.js"></script>
@ -23,65 +23,49 @@
<body>
<header>
<img id="branding-logo" src="chrome://branding/content/aboutlogins.svg" alt=""/>
<login-filter data-l10n-id="login-filter"
data-l10n-attrs="placeholder"></login-filter>
<login-filter></login-filter>
<button id="create-login-button" data-l10n-id="create-login-button"></button>
<menu-button data-l10n-id="menu-button"
data-l10n-attrs="button-title,
menuitem-faq,
menuitem-feedback,
menuitem-import,
menuitem-preferences"></menu-button>
<menu-button></menu-button>
</header>
<login-list data-l10n-id="login-list"
data-l10n-args='{"count": 0}'
data-l10n-attrs="aria-label,
count,
last-changed-option,
last-used-option,
missing-username,
name-option,
new-login-subtitle,
new-login-title,
sort-label-text"></login-list>
<login-item data-l10n-id="login-item"
data-l10n-args='{"timeCreated": 0, "timeChanged": 0, "timeUsed": 0}'
data-l10n-attrs="cancel-button,
copy-password-button,
copy-username-button,
copied-password-button,
copied-username-button,
delete-button,
edit-button,
new-login-title,
open-site-button,
origin-label,
origin-placeholder,
password-hide-title,
password-label,
password-show-title,
save-changes-button,
time-created,
time-changed,
time-used,
username-label,
username-placeholder"></login-item>
<login-list></login-list>
<login-item></login-item>
<confirm-delete-dialog hidden></confirm-delete-dialog>
<template id="confirm-delete-dialog-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.css">
<div class="overlay">
<div class="container" role="dialog" aria-labelledby="title" aria-describedby="message">
<div class="title-bar">
<h1 class="title" id="title" data-l10n-id="confirm-delete-dialog-title"></h1>
<button class="dismiss-button" data-l10n-id="confirm-delete-dialog-dismiss-button"></button>
</div>
<div class="content">
<p class="message" id="message" data-l10n-id="confirm-delete-dialog-message"></p>
</div>
<div class="buttons">
<button class="cancel-button" data-l10n-id="confirm-delete-dialog-cancel-button"></button>
<button class="confirm-button" data-l10n-id="confirm-delete-dialog-confirm-button"></button>
</div>
</div>
</div>
</template>
<template id="login-list-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-list.css">
<div class="meta">
<label for="login-sort">
<span class="sort-label-text"></span>
<span data-l10n-id="login-list-sort-label-text"></span>
<select id="login-sort">
<option class="name-option" value="name"/>
<option class="last-used-option" value="last-used"/>
<option class="last-changed-option" value="last-changed"/>
<option data-l10n-id="login-list-name-option" value="name"/>
<option data-l10n-id="login-list-last-used-option" value="last-used"/>
<option data-l10n-id="login-list-last-changed-option" value="last-changed"/>
</select>
</label>
<span class="count"></span>
<span class="count" data-l10n-id="login-list-count" data-l10n-args='{"count": 0}'></span>
</div>
<ol role="listbox" tabindex="0">
<ol role="listbox" tabindex="0" data-l10n-id="login-list">
</ol>
</template>
@ -97,71 +81,71 @@
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-item.css">
<div class="header">
<h2 class="title"></h2>
<button class="edit-button alternate-button"></button>
<button class="delete-button alternate-button"></button>
<h2 class="title">
<span class="login-item-title"></span>
<span class="new-login-title" data-l10n-id="login-item-new-login-title"></span>
</h2>
<button class="edit-button alternate-button" data-l10n-id="login-item-edit-button"></button>
<button class="delete-button alternate-button" data-l10n-id="login-item-delete-button"></button>
</div>
<form>
<div class="detail-row">
<label class="detail-cell">
<span class="origin-label field-label"></span>
<input type="url" name="origin" required/>
<span class="origin-label field-label" data-l10n-id="login-item-origin-label"></span>
<input type="url" name="origin" required data-l10n-id="login-item-origin"/>
</label>
<button class="open-site-button"></button>
<button class="open-site-button" data-l10n-id="login-item-open-site-button"></button>
</div>
<div class="detail-row">
<label class="detail-cell">
<span class="username-label field-label"></span>
<input type="text" name="username"/>
<span class="username-label field-label" data-l10n-id="login-item-username-label"></span>
<input type="text" name="username" data-l10n-id="login-item-username"/>
</label>
<copy-to-clipboard-button class="copy-username-button"
data-telemetry-object="username"></copy-to-clipboard-button>
<button class="copy-button copy-username-button" data-copy-login-property="username" data-telemetry-object="username">
<span class="copied-button-text" data-l10n-id="login-item-copied-username-button-text"></span>
<span class="copy-button-text" data-l10n-id="login-item-copy-username-button-text"></span>
</button>
</div>
<div class="detail-row">
<label class="detail-cell">
<span class="password-label field-label"></span>
<span class="password-label field-label" data-l10n-id="login-item-password-label"></span>
<div class="reveal-password-wrapper">
<input type="password" name="password" required/>
<input type="checkbox" class="reveal-password-checkbox"/>
<input type="checkbox"
class="reveal-password-checkbox"
data-l10n-id="login-item-password-reveal-checkbox"/>
</div>
</label>
<copy-to-clipboard-button class="copy-password-button"
data-telemetry-object="password"></copy-to-clipboard-button>
<button class="copy-button copy-password-button" data-copy-login-property="password" data-telemetry-object="password">
<span class="copied-button-text" data-l10n-id="login-item-copied-password-button-text"></span>
<span class="copy-button-text" data-l10n-id="login-item-copy-password-button-text"></span>
</button>
</div>
<p class="time-created meta-info"></p>
<p class="time-changed meta-info"></p>
<p class="time-used meta-info"></p>
<button class="save-changes-button"></button>
<button class="cancel-button"></button>
<p class="time-created meta-info" data-l10n-id="login-item-time-created" data-l10n-args='{"timeCreated": 0}'></p>
<p class="time-changed meta-info" data-l10n-id="login-item-time-changed" data-l10n-args='{"timeChanged": 0}'></p>
<p class="time-used meta-info" data-l10n-id="login-item-time-used" data-l10n-args='{"timeUsed": 0}'></p>
<button class="save-changes-button" data-l10n-id="login-item-save-changes-button"></button>
<button class="cancel-button" data-l10n-id="login-item-cancel-button"></button>
</form>
</template>
<template id="login-filter-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/login-filter.css">
<input class="filter" type="text"/>
<input data-l10n-id="login-filter" class="filter" type="text"/>
</template>
<template id="menu-button-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/menu-button.css">
<button class="menu-button alternate-button"></button>
<button class="menu-button alternate-button" data-l10n-id="menu"></button>
<ul class="menu" role="menu" hidden>
<button role="menuitem" class="menuitem-button menuitem-import alternate-button" hidden data-supported-platforms="Win32" data-event-name="AboutLoginsImport"></button>
<button role="menuitem" class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences"></button>
<button role="menuitem" class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback"></button>
<button role="menuitem" class="menuitem-button menuitem-faq alternate-button" data-event-name="AboutLoginsOpenFAQ"></button>
<button role="menuitem" class="menuitem-button menuitem-import alternate-button" hidden data-supported-platforms="Win32" data-event-name="AboutLoginsImport" data-l10n-id="menu-menuitem-import"></button>
<button role="menuitem" class="menuitem-button menuitem-preferences alternate-button" data-event-name="AboutLoginsOpenPreferences" data-l10n-id="menu-menuitem-preferences"></button>
<button role="menuitem" class="menuitem-button menuitem-feedback alternate-button" data-event-name="AboutLoginsOpenFeedback" data-l10n-id="menu-menuitem-feedback"></button>
<button role="menuitem" class="menuitem-button menuitem-faq alternate-button" data-event-name="AboutLoginsOpenFAQ" data-l10n-id="menu-menuitem-faq"></button>
</ul>
</template>
<template id="copy-to-clipboard-button-template">
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css">
<link rel="stylesheet" href="chrome://browser/content/aboutlogins/components/copy-to-clipboard-button.css">
<button class="copy-button">
<span class="copied-button-text"></span>
<span class="copy-button-text"></span>
</button>
</template>
</body>
</html>

View File

@ -0,0 +1,94 @@
:host {
/* these variable values come from about:preferences */
--in-content-dialogtitle-background: #f1f1f1;
--in-content-dialogtitle-border: #c1c1c1;
}
.overlay {
position: fixed;
z-index: 1;
top: 0;
bottom: 0;
left: 0;
right: 0;
/* TODO: this color is used in the about:preferences overlay, but
why isn't it declared as a variable? */
background-color: rgba(0,0,0,0.5);
display: flex;
align-items: center;
}
.container {
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 50%;
min-width: 250px;
max-width: 500px;
height: 40%;
min-height: 200px;
margin: auto;
background: var(--in-content-page-background);
color: var(--in-content-page-color);
}
.title-bar {
position: relative;
flex: 0 1 auto;
text-align: center;
background-color: var(--in-content-dialogtitle-background);
padding: 5px;
border-bottom: 1px solid var(--in-content-dialogtitle-border);
}
.title {
font-size: .9em;
line-height: 1.8em;
font-weight: 600;
-moz-user-select: none;
margin: 0;
}
button.dismiss-button {
position: absolute;
top: 0;
right: 0;
min-width: 20px;
min-height: 20px;
margin: 8px 16px;
padding: 0;
background: url(chrome://global/skin/icons/close.svg) no-repeat center;
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
fill-opacity: 0;
}
button.dismiss-button:dir(rtl) {
right: auto;
left: 0;
}
.content {
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
}
.buttons {
flex: 0 1 auto;
display: flex;
justify-content: space-between;
}
.buttons button {
margin: 0;
}
.content,
.buttons {
margin: 16px;
}

View File

@ -0,0 +1,87 @@
/* 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/. */
export default class ConfirmDeleteDialog extends HTMLElement {
constructor() {
super();
this._promise = null;
}
connectedCallback() {
if (this.shadowRoot) {
return;
}
let template = document.querySelector("#confirm-delete-dialog-template");
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(template.content.cloneNode(true));
this._cancelButton = this.shadowRoot.querySelector(".cancel-button");
this._confirmButton = this.shadowRoot.querySelector(".confirm-button");
this._dismissButton = this.shadowRoot.querySelector(".dismiss-button");
this._message = this.shadowRoot.querySelector(".message");
this._overlay = this.shadowRoot.querySelector(".overlay");
this._title = this.shadowRoot.querySelector(".title");
}
handleEvent(event) {
switch (event.type) {
case "keydown":
if (event.key === "Escape" && !event.defaultPrevented) {
this.onCancel();
}
break;
case "click":
if (event.target.classList.contains("cancel-button") ||
event.target.classList.contains("dismiss-button") ||
event.target.classList.contains("overlay")) {
this.onCancel();
} else if (event.target.classList.contains("confirm-button")) {
this.onConfirm();
}
}
}
hide() {
this._cancelButton.removeEventListener("click", this);
this._confirmButton.removeEventListener("click", this);
this._dismissButton.removeEventListener("click", this);
this._overlay.removeEventListener("click", this);
window.removeEventListener("keydown", this);
this.hidden = true;
}
show() {
this.hidden = false;
this._cancelButton.addEventListener("click", this);
this._confirmButton.addEventListener("click", this);
this._dismissButton.addEventListener("click", this);
this._overlay.addEventListener("click", this);
window.addEventListener("keydown", this);
// For accessibility, focus the least destructive action button when the
// dialog loads.
this._cancelButton.focus();
this._promise = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
return this._promise;
}
onCancel() {
this._reject();
this.hide();
}
onConfirm() {
this._resolve();
this.hide();
}
}
customElements.define("confirm-delete-dialog", ConfirmDeleteDialog);

View File

@ -1,29 +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/. */
:host {
--success-color: #00c100;
}
@supports -moz-bool-pref("browser.in-content.dark-mode") {
@media (prefers-color-scheme: dark) {
:host {
--success-color: #86DE74;
}
}
}
:host(:not([data-copied])) .copied-button-text,
:host([data-copied]) .copy-button-text {
display: none;
}
:host([data-copied]) {
color: var(--success-color);
}
:host([data-copied]) button {
background-color: transparent;
opacity: 1; /* override common.css fading out disabled buttons */
}

View File

@ -1,84 +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 {recordTelemetryEvent} from "../aboutLoginsUtils.js";
import ReflectedFluentElement from "./reflected-fluent-element.js";
export default class CopyToClipboardButton extends ReflectedFluentElement {
/**
* The number of milliseconds to display the "Copied" success message
* before reverting to the normal "Copy" button.
*/
static get BUTTON_RESET_TIMEOUT() {
return 5000;
}
constructor() {
super();
this._relatedInput = null;
}
connectedCallback() {
if (this.shadowRoot) {
return;
}
let CopyToClipboardButtonTemplate = document.querySelector("#copy-to-clipboard-button-template");
this.attachShadow({mode: "open"})
.appendChild(CopyToClipboardButtonTemplate.content.cloneNode(true));
this._copyButton = this.shadowRoot.querySelector(".copy-button");
this._copyButton.addEventListener("click", this);
super.connectedCallback();
}
static get reflectedFluentIDs() {
return ["copy-button-text", "copied-button-text"];
}
static get observedAttributes() {
return CopyToClipboardButton.reflectedFluentIDs;
}
handleSpecialCaseFluentString(attrName) {
if (attrName != "copied-button-text" &&
attrName != "copy-button-text") {
return false;
}
let span = this.shadowRoot.querySelector("." + attrName);
span.textContent = this.getAttribute(attrName);
return true;
}
handleEvent(event) {
if (event.type != "click" || event.currentTarget != this._copyButton) {
return;
}
this._copyButton.disabled = true;
navigator.clipboard.writeText(this._relatedInput.value).then(() => {
this.dataset.copied = true;
setTimeout(() => {
this._copyButton.disabled = false;
delete this.dataset.copied;
}, CopyToClipboardButton.BUTTON_RESET_TIMEOUT);
}, () => this._copyButton.disabled = false);
if (this.dataset.telemetryObject) {
recordTelemetryEvent({object: this.dataset.telemetryObject, method: "copy"});
}
}
/**
* @param {Element} val A reference to the input element whose value will
* be placed on the clipboard.
*/
set relatedInput(val) {
this._relatedInput = val;
}
}
customElements.define("copy-to-clipboard-button", CopyToClipboardButton);

View File

@ -3,23 +3,21 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {recordTelemetryEvent} from "../aboutLoginsUtils.js";
import ReflectedFluentElement from "./reflected-fluent-element.js";
export default class LoginFilter extends ReflectedFluentElement {
export default class LoginFilter extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) {
return;
}
let loginFilterTemplate = document.querySelector("#login-filter-template");
this.attachShadow({mode: "open"})
.appendChild(loginFilterTemplate.content.cloneNode(true));
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(loginFilterTemplate.content.cloneNode(true));
this._input = this.shadowRoot.querySelector("input");
this.addEventListener("input", this);
super.connectedCallback();
}
handleEvent(event) {
@ -31,14 +29,6 @@ export default class LoginFilter extends ReflectedFluentElement {
}
}
static get reflectedFluentIDs() {
return ["placeholder"];
}
static get observedAttributes() {
return this.reflectedFluentIDs;
}
get value() {
return this._input.value;
}
@ -48,16 +38,6 @@ export default class LoginFilter extends ReflectedFluentElement {
this._dispatchFilterEvent(val);
}
handleSpecialCaseFluentString(attrName) {
if (!this.shadowRoot ||
attrName != "placeholder") {
return false;
}
this._input.placeholder = this.getAttribute(attrName);
return true;
}
_dispatchFilterEvent(value) {
this.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
bubbles: true,

View File

@ -8,6 +8,15 @@
--reveal-checkbox-opacity: .8;
--reveal-checkbox-opacity-hover: .6;
--reveal-checkbox-opacity-active: 1;
--success-color: #00c100;
}
@supports -moz-bool-pref("browser.in-content.dark-mode") {
@media (prefers-color-scheme: dark) {
:host {
--success-color: #86DE74;
}
}
}
:host([data-editing]) .edit-button,
@ -16,6 +25,8 @@
:host([data-is-new-login]) copy-to-clipboard-button,
:host([data-is-new-login]) .open-site-button,
:host([data-is-new-login]) .meta-info,
:host([data-is-new-login]) .login-item-title,
:host(:not([data-is-new-login])) .new-login-title,
:host(:not([data-editing])) .cancel-button,
:host(:not([data-editing])) .save-changes-button {
display: none;
@ -90,6 +101,17 @@
margin-bottom: 5px;
}
.copy-button:not([data-copied]) .copied-button-text,
.copy-button[data-copied] .copy-button-text {
display: none;
}
.copy-button[data-copied] {
color: var(--success-color) !important; /* override common.css */
background-color: transparent;
opacity: 1; /* override common.css fading out disabled buttons */
}
.meta-info {
font-size: smaller;
}

View File

@ -3,9 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {recordTelemetryEvent} from "../aboutLoginsUtils.js";
import ReflectedFluentElement from "./reflected-fluent-element.js";
export default class LoginItem extends ReflectedFluentElement {
export default class LoginItem extends HTMLElement {
/**
* The number of milliseconds to display the "Copied" success message
* before reverting to the normal "Copy" button.
*/
static get COPY_BUTTON_RESET_TIMEOUT() {
return 5000;
}
constructor() {
super();
this._login = {};
@ -18,13 +25,17 @@ export default class LoginItem extends ReflectedFluentElement {
}
let loginItemTemplate = document.querySelector("#login-item-template");
this.attachShadow({mode: "open"})
.appendChild(loginItemTemplate.content.cloneNode(true));
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(loginItemTemplate.content.cloneNode(true));
for (let selector of [
".copy-password-button",
".copy-username-button",
".delete-button",
".edit-button",
".open-site-button",
".reveal-password-checkbox",
".save-changes-button",
".cancel-button",
]) {
@ -32,6 +43,7 @@ export default class LoginItem extends ReflectedFluentElement {
button.addEventListener("click", this);
}
this._confirmDeleteDialog = document.querySelector("confirm-delete-dialog");
this._copyPasswordButton = this.shadowRoot.querySelector(".copy-password-button");
this._copyUsernameButton = this.shadowRoot.querySelector(".copy-username-button");
this._deleteButton = this.shadowRoot.querySelector(".delete-button");
@ -41,98 +53,23 @@ export default class LoginItem extends ReflectedFluentElement {
this._usernameInput = this.shadowRoot.querySelector("input[name='username']");
this._passwordInput = this.shadowRoot.querySelector("input[name='password']");
this._revealCheckbox = this.shadowRoot.querySelector(".reveal-password-checkbox");
this._title = this.shadowRoot.querySelector(".title");
this._copyUsernameButton.relatedInput = this._usernameInput;
this._copyPasswordButton.relatedInput = this._passwordInput;
this._title = this.shadowRoot.querySelector(".login-item-title");
this._timeCreated = this.shadowRoot.querySelector(".time-created");
this._timeChanged = this.shadowRoot.querySelector(".time-changed");
this._timeUsed = this.shadowRoot.querySelector(".time-used");
this.render();
this._originInput.addEventListener("blur", this);
this._revealCheckbox.addEventListener("click", this);
window.addEventListener("AboutLoginsLoginSelected", this);
super.connectedCallback();
}
static get reflectedFluentIDs() {
return [
"cancel-button",
"copied-password-button",
"copied-username-button",
"copy-password-button",
"copy-username-button",
"delete-button",
"edit-button",
"new-login-title",
"open-site-button",
"origin-label",
"origin-placeholder",
"password-hide-title",
"password-label",
"password-show-title",
"save-changes-button",
"time-created",
"time-changed",
"time-used",
"username-label",
"username-placeholder",
];
}
static get observedAttributes() {
return this.reflectedFluentIDs;
}
handleSpecialCaseFluentString(attrName) {
switch (attrName) {
case "copied-password-button":
case "copy-password-button": {
let newAttrName = attrName.substr(0, attrName.indexOf("-")) + "-button-text";
this._copyPasswordButton.setAttribute(newAttrName, this.getAttribute(attrName));
break;
}
case "copied-username-button":
case "copy-username-button": {
let newAttrName = attrName.substr(0, attrName.indexOf("-")) + "-button-text";
this._copyUsernameButton.setAttribute(newAttrName, this.getAttribute(attrName));
break;
}
case "new-login-title": {
this._title.setAttribute(attrName, this.getAttribute(attrName));
if (!this._login.title) {
this._title.textContent = this.getAttribute(attrName);
}
break;
}
case "origin-placeholder": {
this._originInput.setAttribute("placeholder", this.getAttribute(attrName));
break;
}
case "password-hide-title":
case "password-show-title": {
this._updatePasswordRevealState();
break;
}
case "username-placeholder": {
this._usernameInput.setAttribute("placeholder", this.getAttribute(attrName));
break;
}
default:
return false;
}
return true;
}
render() {
let l10nArgs = {
timeCreated: this._login.timeCreated || "",
timeChanged: this._login.timePasswordChanged || "",
timeUsed: this._login.timeLastUsed || "",
};
document.l10n.setAttributes(this, "login-item", l10nArgs);
document.l10n.setAttributes(this._timeCreated, "login-item-time-created", {timeCreated: this._login.timeCreated || ""});
document.l10n.setAttributes(this._timeChanged, "login-item-time-changed", {timeChanged: this._login.timePasswordChanged || ""});
document.l10n.setAttributes(this._timeUsed, "login-item-time-used", {timeUsed: this._login.timeLastUsed || ""});
this._title.textContent = this._login.title || this._title.getAttribute("new-login-title");
this._title.textContent = this._login.title;
this._originInput.defaultValue = this._login.origin || "";
this._usernameInput.defaultValue = this._login.username || "";
this._passwordInput.defaultValue = this._login.password || "";
@ -157,7 +94,7 @@ export default class LoginItem extends ReflectedFluentElement {
break;
}
case "click": {
let classList = event.target.classList;
let classList = event.currentTarget.classList;
if (classList.contains("reveal-password-checkbox")) {
this._updatePasswordRevealState();
@ -184,13 +121,24 @@ export default class LoginItem extends ReflectedFluentElement {
});
return;
}
if (classList.contains("delete-button")) {
document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
bubbles: true,
detail: this._login,
}));
if (classList.contains("copy-password-button") ||
classList.contains("copy-username-button")) {
let copyButton = event.currentTarget;
copyButton.disabled = true;
let propertyToCopy = copyButton.dataset.copyLoginProperty;
navigator.clipboard.writeText(this._login[propertyToCopy]).then(() => {
copyButton.dataset.copied = true;
setTimeout(() => {
copyButton.disabled = false;
delete copyButton.dataset.copied;
}, LoginItem.COPY_BUTTON_RESET_TIMEOUT);
}, () => copyButton.disabled = false);
recordTelemetryEvent({object: "existing_login", method: "delete"});
recordTelemetryEvent({object: copyButton.dataset.telemetryObject, method: "copy"});
return;
}
if (classList.contains("delete-button")) {
this.confirmDelete();
return;
}
if (classList.contains("edit-button")) {
@ -235,6 +183,21 @@ export default class LoginItem extends ReflectedFluentElement {
}
}
/**
* Toggles the confirm delete dialog, completing the deletion if the user
* agrees.
*/
confirmDelete() {
const dialog = document.querySelector("confirm-delete-dialog");
dialog.show().then(() => {
document.dispatchEvent(new CustomEvent("AboutLoginsDeleteLogin", {
bubbles: true,
detail: this._login,
}));
recordTelemetryEvent({object: "existing_login", method: "delete"});
}, () => {});
}
/**
* @param {login} login The login that should be displayed. The login object is
* a plain JS object representation of nsILoginInfo/nsILoginMetaInfo.
@ -381,13 +344,12 @@ export default class LoginItem extends ReflectedFluentElement {
}
_updatePasswordRevealState() {
let labelAttr = this._revealCheckbox.checked ? "password-show-title"
: "password-hide-title";
this._revealCheckbox.setAttribute("aria-label", this.getAttribute(labelAttr));
this._revealCheckbox.setAttribute("title", this.getAttribute(labelAttr));
let titleId = this._revealCheckbox.checked ? "login-item-password-reveal-checkbox-hide"
: "login-item-password-reveal-checkbox-show";
document.l10n.setAttributes(this._revealCheckbox, titleId);
let {checked} = this._revealCheckbox;
let inputType = checked ? "type" : "password";
let inputType = checked ? "text" : "password";
this._passwordInput.setAttribute("type", inputType);
}
}

View File

@ -21,29 +21,35 @@ export default class LoginListItem extends HTMLElement {
}
let loginListItemTemplate = document.querySelector("#login-list-item-template");
this.attachShadow({mode: "open"})
.appendChild(loginListItemTemplate.content.cloneNode(true));
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(loginListItemTemplate.content.cloneNode(true));
this._title = this.shadowRoot.querySelector(".title");
this._username = this.shadowRoot.querySelector(".username");
this.setAttribute("role", "option");
this.render();
this.addEventListener("click", this);
this.render();
}
render() {
if (!this._login.guid) {
delete this.dataset.guid;
this._title.textContent = this.getAttribute("new-login-title");
this._username.textContent = this.getAttribute("new-login-subtitle");
document.l10n.setAttributes(this._title, "login-list-item-title-new-login");
document.l10n.setAttributes(this._username, "login-list-item-subtitle-new-login");
return;
}
this.dataset.guid = this._login.guid;
this._title.textContent = this._login.title;
this._username.textContent = this._login.username.trim() || this.getAttribute("missing-username");
if (this._login.username.trim()) {
this._username.removeAttribute("data-l10n-id");
this._username.textContent = this._login.username.trim();
} else {
document.l10n.setAttributes(this._username, "login-list-item-subtitle-missing-username");
}
}
handleEvent(event) {

View File

@ -3,7 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import LoginListItem from "./login-list-item.js";
import ReflectedFluentElement from "./reflected-fluent-element.js";
const collator = new Intl.Collator();
const sortFnOptions = {
@ -12,7 +11,7 @@ const sortFnOptions = {
"last-changed": (a, b) => (a.timePasswordChanged < b.timePasswordChanged),
};
export default class LoginList extends ReflectedFluentElement {
export default class LoginList extends HTMLElement {
constructor() {
super();
this._logins = [];
@ -26,10 +25,12 @@ export default class LoginList extends ReflectedFluentElement {
return;
}
let loginListTemplate = document.querySelector("#login-list-template");
this.attachShadow({mode: "open"})
.appendChild(loginListTemplate.content.cloneNode(true));
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(loginListTemplate.content.cloneNode(true));
this._list = this.shadowRoot.querySelector("ol");
this._count = this.shadowRoot.querySelector(".count");
this.render();
@ -38,15 +39,13 @@ export default class LoginList extends ReflectedFluentElement {
window.addEventListener("AboutLoginsLoginSelected", this);
window.addEventListener("AboutLoginsFilterLogins", this);
this.addEventListener("keydown", this);
super.connectedCallback();
}
render() {
this._list.textContent = "";
if (!this._logins.length) {
document.l10n.setAttributes(this, "login-list", {count: 0});
document.l10n.setAttributes(this._count, "login-list-count", {count: 0});
return;
}
@ -59,7 +58,6 @@ export default class LoginList extends ReflectedFluentElement {
for (let login of this._logins) {
let listItem = new LoginListItem(login);
listItem.setAttribute("missing-username", this.getAttribute("missing-username"));
if (login.guid == this._selectedGuid) {
listItem.classList.add("selected");
listItem.setAttribute("aria-selected", "true");
@ -69,7 +67,7 @@ export default class LoginList extends ReflectedFluentElement {
}
let visibleLoginCount = this._applyFilter();
document.l10n.setAttributes(this, "login-list", {count: visibleLoginCount});
document.l10n.setAttributes(this._count, "login-list-count", {count: visibleLoginCount});
}
handleEvent(event) {
@ -101,42 +99,6 @@ export default class LoginList extends ReflectedFluentElement {
}
}
static get reflectedFluentIDs() {
return ["aria-label",
"count",
"last-used-option",
"last-changed-option",
"missing-username",
"name-option",
"new-login-subtitle",
"new-login-title",
"sort-label-text"];
}
static get observedAttributes() {
return this.reflectedFluentIDs;
}
handleSpecialCaseFluentString(attrName) {
switch (attrName) {
case "aria-label": {
this._list.setAttribute("aria-label", this.getAttribute(attrName));
break;
}
case "missing-username": {
break;
}
case "new-login-subtitle":
case "new-login-title": {
this._blankLoginListItem.setAttribute(attrName, this.getAttribute(attrName));
break;
}
default:
return false;
}
return true;
}
/**
* @param {login[]} logins An array of logins used for displaying in the list.
*/

View File

@ -2,17 +2,16 @@
* 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 ReflectedFluentElement from "chrome://browser/content/aboutlogins/components/reflected-fluent-element.js";
export default class MenuButton extends ReflectedFluentElement {
export default class MenuButton extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) {
return;
}
let MenuButtonTemplate = document.querySelector("#menu-button-template");
this.attachShadow({mode: "open"})
.appendChild(MenuButtonTemplate.content.cloneNode(true));
let shadowRoot = this.attachShadow({mode: "open"});
document.l10n.connectRoot(shadowRoot);
shadowRoot.appendChild(MenuButtonTemplate.content.cloneNode(true));
for (let menuitem of this.shadowRoot.querySelectorAll(".menuitem-button[data-supported-platforms]")) {
let supportedPlatforms = menuitem.dataset.supportedPlatforms.split(",").map(platform => platform.trim());
@ -27,32 +26,6 @@ export default class MenuButton extends ReflectedFluentElement {
this.addEventListener("blur", this);
this._menuButton.addEventListener("click", this);
this.addEventListener("keydown", this, true);
super.connectedCallback();
}
static get reflectedFluentIDs() {
return [
"button-title",
"menuitem-faq",
"menuitem-import",
"menuitem-feedback",
"menuitem-preferences",
];
}
static get observedAttributes() {
return MenuButton.reflectedFluentIDs;
}
handleSpecialCaseFluentString(attrName) {
if (!this.shadowRoot ||
attrName != "button-title") {
return false;
}
this._menuButton.setAttribute("title", this.getAttribute(attrName));
return true;
}
handleEvent(event) {

View File

@ -1,60 +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/. */
export default class ReflectedFluentElement extends HTMLElement {
connectedCallback() {
this._reflectFluentStrings();
}
/*
* Fluent doesn't handle localizing into Shadow DOM yet so strings
* need to get reflected in to their targeted element.
*/
attributeChangedCallback(attr, oldValue, newValue) {
if (!this.shadowRoot) {
return;
}
// Don't respond to attribute changes that aren't related to locale text.
if (!this.constructor.reflectedFluentIDs.includes(attr)) {
return;
}
if (this.handleSpecialCaseFluentString &&
this.handleSpecialCaseFluentString(attr)) {
return;
}
// Strings that are reflected to their shadowed element are assigned
// to an attribute name that matches a className on the element.
let shadowedElement = this.shadowRoot.querySelector("." + attr);
shadowedElement.textContent = newValue;
}
_isReflectedAttributePresent(attr) {
return this.constructor.reflectedFluentIDs.includes(attr.name);
}
/*
* Called to apply any localized strings that Fluent may have applied
* to the element before the custom element was defined.
*/
_reflectFluentStrings() {
for (let reflectedFluentID of this.constructor.reflectedFluentIDs) {
if (this.hasAttribute(reflectedFluentID)) {
if (this.handleSpecialCaseFluentString &&
this.handleSpecialCaseFluentString(reflectedFluentID)) {
continue;
}
let attrValue = this.getAttribute(reflectedFluentID);
// Strings that are reflected to their shadowed element are assigned
// to an attribute name that matches a className on the element.
let shadowedElement = this.shadowRoot.querySelector("." + reflectedFluentID);
shadowedElement.textContent = attrValue;
}
}
}
}
customElements.define("reflected-fluent-element", ReflectedFluentElement);

View File

@ -3,8 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/aboutlogins/components/copy-to-clipboard-button.css (content/components/copy-to-clipboard-button.css)
content/browser/aboutlogins/components/copy-to-clipboard-button.js (content/components/copy-to-clipboard-button.js)
content/browser/aboutlogins/components/confirm-delete-dialog.css (content/components/confirm-delete-dialog.css)
content/browser/aboutlogins/components/confirm-delete-dialog.js (content/components/confirm-delete-dialog.js)
content/browser/aboutlogins/components/login-filter.css (content/components/login-filter.css)
content/browser/aboutlogins/components/login-filter.js (content/components/login-filter.js)
content/browser/aboutlogins/components/login-item.css (content/components/login-item.css)
@ -15,7 +15,6 @@ browser.jar:
content/browser/aboutlogins/components/login-list-item.js (content/components/login-list-item.js)
content/browser/aboutlogins/components/menu-button.css (content/components/menu-button.css)
content/browser/aboutlogins/components/menu-button.js (content/components/menu-button.js)
content/browser/aboutlogins/components/reflected-fluent-element.js (content/components/reflected-fluent-element.js)
content/browser/aboutlogins/icons/delete.svg (content/icons/delete.svg)
content/browser/aboutlogins/icons/edit.svg (content/icons/edit.svg)
content/browser/aboutlogins/icons/faq.svg (content/icons/faq.svg)

View File

@ -8,6 +8,7 @@ support-files =
# Skip ASAN and debug since waiting for content events is already slow.
[browser_aaa_eventTelemetry_run_first.js]
skip-if = asan || debug
[browser_confirmDeleteDialog.js]
[browser_copyToClipboardButton.js]
[browser_createLogin.js]
[browser_deleteLogin.js]

View File

@ -43,7 +43,6 @@ add_task(async function test_telemetry_events() {
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
let loginItem = content.document.querySelector("login-item");
let copyButton = loginItem.shadowRoot.querySelector(".copy-username-button");
copyButton = copyButton.shadowRoot.querySelector(".copy-button");
copyButton.click();
});
await waitForTelemetryEventCount(2);
@ -51,7 +50,6 @@ add_task(async function test_telemetry_events() {
await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
let loginItem = content.document.querySelector("login-item");
let copyButton = loginItem.shadowRoot.querySelector(".copy-password-button");
copyButton = copyButton.shadowRoot.querySelector(".copy-button");
copyButton.click();
});
await waitForTelemetryEventCount(3);
@ -108,6 +106,9 @@ add_task(async function test_telemetry_events() {
let loginItem = content.document.querySelector("login-item");
let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
deleteButton.click();
let confirmDeleteDialog = content.document.querySelector("confirm-delete-dialog");
let confirmDeleteButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
confirmDeleteButton.click();
});
await waitForTelemetryEventCount(10);

View File

@ -0,0 +1,68 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(async function setup() {
await BrowserTestUtils.openNewForegroundTab({gBrowser, url: "about:logins"});
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});
});
add_task(async function test() {
let browser = gBrowser.selectedBrowser;
await ContentTask.spawn(browser, null, async () => {
let dialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
let cancelButton = dialog.shadowRoot.querySelector(".cancel-button");
let confirmDeleteButton = dialog.shadowRoot.querySelector(".confirm-button");
let dismissButton = dialog.shadowRoot.querySelector(".dismiss-button");
let message = dialog.shadowRoot.querySelector(".message");
let title = dialog.shadowRoot.querySelector(".title");
is(title.textContent, "Confirm Deletion",
"Title contents should match l10n attribute set on outer element");
is(message.textContent, "Are you sure you want to delete this login?",
"Message contents should match l10n attribute set on outer element");
is(cancelButton.textContent, "Cancel",
"Cancel button contents should match l10n attribute set on outer element");
is(confirmDeleteButton.textContent, "Delete login",
"Delete button contents should match l10n attribute set on outer element");
let showPromise = dialog.show();
cancelButton.click();
try {
await showPromise;
ok(false, "Promise returned by show() should not resolve after clicking cancel button");
} catch (ex) {
ok(true, "Promise returned by show() should reject after clicking cancel button");
}
await ContentTaskUtils.waitForCondition(() => dialog.hidden,
"Waiting for the dialog to be hidden");
ok(dialog.hidden, "Dialog should be hidden after clicking cancel button");
showPromise = dialog.show();
dismissButton.click();
try {
await showPromise;
ok(false, "Promise returned by show() should not resolve after clicking dismiss button");
} catch (ex) {
ok(true, "Promise returned by show() should reject after clicking dismiss button");
}
await ContentTaskUtils.waitForCondition(() => dialog.hidden,
"Waiting for the dialog to be hidden");
ok(dialog.hidden, "Dialog should be hidden after clicking dismiss button");
showPromise = dialog.show();
confirmDeleteButton.click();
try {
await showPromise;
ok(true, "Promise returned by show() should resolve after clicking confirm button");
} catch (ex) {
ok(false, "Promise returned by show() should not reject after clicking confirm button");
}
await ContentTaskUtils.waitForCondition(() => dialog.hidden,
"Waiting for the dialog to be hidden");
ok(dialog.hidden, "Dialog should be hidden after clicking confirm button");
});
});

View File

@ -19,8 +19,7 @@ add_task(async function test() {
loginItem.setLogin(Cu.cloneInto(login, content));
// Lower the timeout for the test.
let copyButton = loginItem.shadowRoot.querySelector(".copy-username-button");
Object.defineProperty(copyButton.constructor, "BUTTON_RESET_TIMEOUT", {
Object.defineProperty(loginItem.constructor, "COPY_BUTTON_RESET_TIMEOUT", {
configurable: true,
writable: true,
value: 1000,
@ -40,9 +39,8 @@ add_task(async function test() {
await ContentTask.spawn(browser, testObj, async function(aTestObj) {
let loginItem = content.document.querySelector("login-item");
let copyButton = loginItem.shadowRoot.querySelector(aTestObj.copyButtonSelector);
let innerButton = copyButton.shadowRoot.querySelector("button");
info("Clicking 'copy' button");
innerButton.click();
copyButton.click();
});
});
ok(true, testObj.expectedValue + " is on clipboard now");

View File

@ -45,6 +45,10 @@ add_task(async function test_login_item() {
let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
deleteButton.click();
let confirmDeleteDialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
let confirmButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
confirmButton.click();
});
ok(deleteLoginMessageReceived,
"Clicking the delete button should send the AboutLogins:DeleteLogin messsage");

View File

@ -80,6 +80,9 @@ add_task(async function test_login_item() {
ok(loginItem.dataset.editing, "LoginItem should be in 'edit' mode");
let deleteButton = loginItem.shadowRoot.querySelector(".delete-button");
deleteButton.click();
let confirmDeleteDialog = Cu.waiveXrays(content.document.querySelector("confirm-delete-dialog"));
let confirmDeleteButton = confirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
confirmDeleteButton.click();
await ContentTaskUtils.waitForCondition(() => {
loginListItem = Cu.waiveXrays(loginList.shadowRoot.querySelector("login-list-item"));

View File

@ -1,6 +1,6 @@
"use strict";
/* exported asyncElementRendered, importDependencies, stubFluentL10n */
/* exported asyncElementRendered, importDependencies */
/**
* A helper to await on while waiting for an asynchronous rendering of a Custom
@ -25,18 +25,24 @@ function importDependencies(templateFrame, destinationEl) {
}
}
function stubFluentL10n(argsMap) {
Object.defineProperty(document, "l10n", {
configurable: true,
writable: true,
value: {
setAttributes(element, id, args) {
element.setAttribute("data-l10n-id", id);
for (let attrName of Object.keys(argsMap)) {
let varName = argsMap[attrName];
element.setAttribute(attrName, args[varName]);
}
},
Object.defineProperty(document, "l10n", {
configurable: true,
writable: true,
value: {
connectRoot() {
},
});
}
translateElements() {
return Promise.resolve();
},
getAttributes(element) {
return {
id: element.getAttribute("data-l10n-id"),
args: JSON.parse(element.getAttribute("data-l10n-args")),
};
},
setAttributes(element, id, args) {
element.setAttribute("data-l10n-id", id);
element.setAttribute("data-l10n-args", JSON.stringify(args));
},
},
});

View File

@ -3,8 +3,8 @@ scheme = https
support-files =
aboutlogins_common.js
[test_confirm_delete_dialog.html]
[test_login_filter.html]
[test_login_item.html]
[test_login_list.html]
[test_menu_button.html]
[test_reflected_fluent_element.html]

View File

@ -0,0 +1,95 @@
<!DOCTYPE HTML>
<html>
<!--
Test the confirm-delete-dialog component
-->
<head>
<meta charset="utf-8">
<title>Test the confirm-delete-dialog component</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/confirm-delete-dialog.js"></script>
<script src="aboutlogins_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
</p>
<div id="content" style="display: none">
<iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
sandbox="allow-same-origin"></iframe>
</div>
<pre id="test">
</pre>
<script>
/** Test the confirm-delete-dialog component **/
let cancelButton, confirmButton, gConfirmDeleteDialog;
add_task(async function setup() {
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
gConfirmDeleteDialog = document.createElement("confirm-delete-dialog");
displayEl.appendChild(gConfirmDeleteDialog);
ok(gConfirmDeleteDialog, "The dialog should exist");
cancelButton = gConfirmDeleteDialog.shadowRoot.querySelector(".cancel-button");
confirmButton = gConfirmDeleteDialog.shadowRoot.querySelector(".confirm-button");
ok(cancelButton, "The cancel button should exist");
ok(confirmButton, "The confirm button should exist");
});
add_task(async function test_escape_key_to_cancel() {
gConfirmDeleteDialog.show();
ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
sendKey("ESCAPE");
ok(gConfirmDeleteDialog.hidden, "The dialog should be hidden after hitting Escape");
gConfirmDeleteDialog.hide();
});
add_task(async function test_initial_focus() {
gConfirmDeleteDialog.show();
ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
is(gConfirmDeleteDialog.shadowRoot.activeElement, cancelButton,
"After initially opening the dialog, the cancel button should be focused");
gConfirmDeleteDialog.hide();
});
add_task(async function test_tab_focus() {
gConfirmDeleteDialog.show();
ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
sendKey("TAB");
is(gConfirmDeleteDialog.shadowRoot.activeElement, confirmButton,
"After opening the dialog and tabbing once, the confirm delete button should be focused");
gConfirmDeleteDialog.hide();
});
add_task(async function test_enter_key_to_cancel() {
let showPromise = gConfirmDeleteDialog.show();
ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
sendKey("RETURN");
try {
await showPromise;
ok(false, "The dialog Promise should not resolve after hitting Return with the cancel button focused");
} catch (ex) {
ok(true, "The dialog Promise should reject after hitting Return with the cancel button focused");
}
});
add_task(async function test_enter_key_to_confirm() {
let showPromise = gConfirmDeleteDialog.show();
ok(!gConfirmDeleteDialog.hidden, "The dialog should be visible");
sendKey("TAB");
sendKey("RETURN");
try {
await showPromise;
ok(true, "The dialog Promise should resolve after hitting Return with the confirm button focused");
} catch (ex) {
ok(false, "The dialog Promise should not reject after hitting Return with the confirm button focused");
}
});
</script>
</body>
</html>

View File

@ -29,10 +29,6 @@ Test the login-filter component
let gLoginFilter;
let gLoginList;
add_task(async function setup() {
stubFluentL10n({
"count": "count",
});
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
@ -113,12 +109,10 @@ add_task(async function test_list_filtered() {
sendString(testObj.query);
await SimpleTest.promiseWaitForCondition(() => {
return gLoginList.hasAttribute("count") &&
+gLoginList.getAttribute("count") == testObj.resultExpectedCount;
let countElement = gLoginList.shadowRoot.querySelector(".count");
return countElement.hasAttribute("data-l10n-args") &&
JSON.parse(countElement.getAttribute("data-l10n-args")).count == testObj.resultExpectedCount;
}, `Waiting for the search result count to update to ${testObj.resultExpectedCount} (tc#${testObj.testCase})`);
let count = +gLoginList.getAttribute("count");
is(count, testObj.resultExpectedCount,
`The login list count should match the expected result (tc#${testObj.testCase})`);
}
});
</script>

View File

@ -47,12 +47,6 @@ const TEST_LOGIN_2 = {
};
add_task(async function setup() {
stubFluentL10n({
"time-created": "timeCreated",
"time-changed": "timeChanged",
"time-used": "timeUsed",
});
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
@ -66,9 +60,9 @@ add_task(async function test_empty_item() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be blank");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be blank");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be blank");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
});
add_task(async function test_set_login() {
@ -80,9 +74,9 @@ add_task(async function test_set_login() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
});
add_task(async function test_edit_login() {
@ -98,9 +92,9 @@ add_task(async function test_edit_login() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be populated");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be populated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be populated");
gLoginItem.shadowRoot.querySelector("input[name='username']").value = "newUsername";
gLoginItem.shadowRoot.querySelector("input[name='password']").value = "newPassword";
@ -158,9 +152,9 @@ add_task(async function test_set_login_empty() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be empty");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be empty");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be empty");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
let createEventDispatched = false;
document.addEventListener("AboutLoginsCreateLogin", event => {
@ -211,9 +205,9 @@ add_task(async function test_different_login_modified() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
});
add_task(async function test_different_login_removed() {
@ -225,9 +219,9 @@ add_task(async function test_different_login_removed() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, TEST_LOGIN_1.origin, "origin should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, TEST_LOGIN_1.username, "username should be unchanged");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, TEST_LOGIN_1.password, "password should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, TEST_LOGIN_1.timeCreated, "time-created should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, TEST_LOGIN_1.timePasswordChanged, "time-changed should be unchanged");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, TEST_LOGIN_1.timeLastUsed, "time-used should be unchanged");
});
add_task(async function test_login_modified() {
@ -239,9 +233,9 @@ add_task(async function test_login_modified() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, modifiedLogin.origin, "origin should be updated");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, modifiedLogin.username, "username should be updated");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, modifiedLogin.password, "password should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, modifiedLogin.timeCreated, "time-created should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, modifiedLogin.timePasswordChanged, "time-changed should be updated");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, modifiedLogin.timeLastUsed, "time-used should be updated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, modifiedLogin.timeCreated, "time-created should be updated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, modifiedLogin.timePasswordChanged, "time-changed should be updated");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, modifiedLogin.timeLastUsed, "time-used should be updated");
});
add_task(async function test_login_removed() {
@ -252,9 +246,9 @@ add_task(async function test_login_removed() {
is(gLoginItem.shadowRoot.querySelector("input[name='origin']").value, "", "origin should be cleared");
is(gLoginItem.shadowRoot.querySelector("input[name='username']").value, "", "username should be cleared");
is(gLoginItem.shadowRoot.querySelector("input[name='password']").value, "", "password should be cleared");
is(gLoginItem.shadowRoot.querySelector(".time-created").textContent, "", "time-created should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-changed").textContent, "", "time-changed should be blank when undefined");
is(gLoginItem.shadowRoot.querySelector(".time-used").textContent, "", "time-used should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-created")).args.timeCreated, "", "time-created should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-changed")).args.timeChanged, "", "time-changed should be blank when undefined");
is(document.l10n.getAttributes(gLoginItem.shadowRoot.querySelector(".time-used")).args.timeUsed, "", "time-used should be blank when undefined");
});
</script>

View File

@ -58,10 +58,6 @@ const TEST_LOGIN_3 = {
};
add_task(async function setup() {
stubFluentL10n({
"count": "count",
});
let templateFrame = document.getElementById("templateFrame");
let displayEl = document.getElementById("display");
importDependencies(templateFrame, displayEl);
@ -134,10 +130,9 @@ add_task(async function test_empty_login_username_in_list() {
ok(!loginListItems[0].dataset.guid, "first login-list-item should be the 'new' item");
is(loginListItems[1].dataset.guid, TEST_LOGIN_3.guid, "login-list-item should have correct guid attribute");
loginListItems[1].setAttribute("missing-username", "(no username)");
loginListItems[1].render();
let loginUsername = loginListItems[1].shadowRoot.querySelector(".username");
is(loginUsername.textContent, "(no username)", "login should show missing username text");
is(loginUsername.getAttribute("data-l10n-id"), "login-list-item-subtitle-missing-username", "login should show missing username text");
});
add_task(async function test_populated_list() {
@ -163,12 +158,12 @@ add_task(async function test_populated_list() {
add_task(async function test_filtered_list() {
is(gLoginList.shadowRoot.querySelectorAll("login-list-item:not([hidden])").length, 2, "Both logins should be visible");
let countSpan = gLoginList.shadowRoot.querySelector(".count");
is(countSpan.textContent, "2", "Count should match full list length");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
bubbles: true,
detail: "user1",
}));
is(countSpan.textContent, "1", "Count should match result amount");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
let loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
is(loginListItems[0].shadowRoot.querySelector(".username").textContent, "user1", "user1 is expected first");
ok(!loginListItems[0].hidden, "user1 should remain visible");
@ -177,7 +172,7 @@ add_task(async function test_filtered_list() {
bubbles: true,
detail: "user2",
}));
is(countSpan.textContent, "1", "Count should match result amount");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
ok(loginListItems[0].hidden, "user1 should be hidden");
ok(!loginListItems[1].hidden, "user2 should be visible");
@ -185,7 +180,7 @@ add_task(async function test_filtered_list() {
bubbles: true,
detail: "user",
}));
is(countSpan.textContent, "2", "Count should match result amount");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
ok(!loginListItems[0].hidden, "user1 should be visible");
ok(!loginListItems[1].hidden, "user2 should be visible");
@ -193,7 +188,7 @@ add_task(async function test_filtered_list() {
bubbles: true,
detail: "foo",
}));
is(countSpan.textContent, "0", "Count should match result amount");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 0, "Count should match result amount");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
ok(loginListItems[0].hidden, "user1 should be hidden");
ok(loginListItems[1].hidden, "user2 should be hidden");
@ -201,7 +196,7 @@ add_task(async function test_filtered_list() {
bubbles: true,
detail: "",
}));
is(countSpan.textContent, "2", "Count should be reset to full list length");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should be reset to full list length");
loginListItems = gLoginList.shadowRoot.querySelectorAll("login-list-item");
ok(!loginListItems[0].hidden, "user1 should be visible");
ok(!loginListItems[1].hidden, "user2 should be visible");
@ -248,13 +243,13 @@ add_task(async function test_login_removed() {
add_task(async function test_login_added_filtered() {
let countSpan = gLoginList.shadowRoot.querySelector(".count");
is(countSpan.textContent, "2", "Count should match full list length");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 2, "Count should match full list length");
window.dispatchEvent(new CustomEvent("AboutLoginsFilterLogins", {
bubbles: true,
composed: true,
detail: "user1",
}));
is(countSpan.textContent, "1", "Count should match result amount");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should match result amount");
let newLogin = Object.assign({}, TEST_LOGIN_1, {username: "user22", guid: "111222"});
gLoginList.loginAdded(newLogin);
@ -267,7 +262,7 @@ add_task(async function test_login_added_filtered() {
ok(!loginListItems[0].hidden, "login-list-item1 should be visible");
ok(loginListItems[1].hidden, "login-list-item2 should be hidden");
ok(loginListItems[2].hidden, "login-list-item3 should be hidden");
is(countSpan.textContent, "1", "Count should remain unchanged");
is(JSON.parse(countSpan.getAttribute("data-l10n-args")).count, 1, "Count should remain unchanged");
});
add_task(async function test_sorted_list() {

View File

@ -1,117 +0,0 @@
<!DOCTYPE HTML>
<html>
<!--
Test the reflected-fluent-element component
-->
<head>
<meta charset="utf-8">
<title>Test the reflected-fluent-element component</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="aboutlogins_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<p id="display">
</p>
<div id="content" style="display: none">
<iframe id="templateFrame" src="chrome://browser/content/aboutlogins/aboutLogins.html"
sandbox="allow-same-origin"></iframe>
</div>
<pre id="test">
</pre>
<script>
/** Test the reflected-fluent-element component **/
const TEST_STRINGS = {
loginFilter: {
placeholder: "Sample placeholder",
},
loginItem: {
"cancel-button": "Cancel",
"delete-button": "Delete",
"origin-label": "Website Address",
"password-label": "Password",
"save-changes-button": "Save Changes",
// See stubFluentL10n for the following three
"time-created": "",
"time-changed": "",
"time-used": "",
"username-label": "Username",
},
};
let gLoginFilter;
let gLoginItem;
add_task(async function setup() {
stubFluentL10n({
"time-created": "timeCreated",
"time-changed": "timeChanged",
"time-used": "timeUsed",
});
let displayEl = document.getElementById("display");
// Create and append the login-filter element before its template
// is cloned the custom element defined.
gLoginFilter = document.createElement("login-filter");
gLoginFilter.setAttribute("placeholder", TEST_STRINGS.loginFilter.placeholder);
displayEl.appendChild(gLoginFilter);
// ... and do the same with the login-item.
gLoginItem = document.createElement("login-item");
for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
gLoginItem.setAttribute(attrKey, TEST_STRINGS.loginItem[attrKey]);
}
displayEl.appendChild(gLoginItem);
let templateFrame = document.getElementById("templateFrame");
importDependencies(templateFrame, displayEl);
// The script needs to be inserted after the element and template are appended
// to match the environment of the locale text being applied before the custom
// element is defined.
for (let scriptSrc of ["login-filter.js", "login-item.js", "login-list.js"]) {
let scriptEl = document.createElement("script");
scriptEl.setAttribute("src", `chrome://browser/content/aboutlogins/components/${scriptSrc}`);
scriptEl.setAttribute("type", "module");
document.head.appendChild(scriptEl);
}
});
add_task(async function test_placeholder_on_login_filter() {
ok(gLoginFilter, "loginFilter exists");
await SimpleTest.promiseWaitForCondition(() => !!gLoginFilter.shadowRoot, "Wait for shadowRoot");
is(gLoginFilter.shadowRoot.querySelector("input").placeholder,
TEST_STRINGS.loginFilter.placeholder,
"Placeholder text should be present when set before the element is defined");
});
add_task(async function test_login_item() {
ok(gLoginItem, "loginItem exists");
await SimpleTest.promiseWaitForCondition(() => !!gLoginItem.shadowRoot, "Wait for shadowRoot");
for (let attrKey of Object.keys(TEST_STRINGS.loginItem)) {
let selector = "." + attrKey;
is(gLoginItem.shadowRoot.querySelector(selector).textContent,
TEST_STRINGS.loginItem[attrKey],
selector + " textContent should be present when set before the element is defined");
}
});
add_task(async function test_attribute_changed_callback() {
let displayEl = document.getElementById("display");
let loginList = document.createElement("login-list");
displayEl.appendChild(loginList);
await SimpleTest.promiseWaitForCondition(() => !!loginList.shadowRoot, "Wait for element to get templated");
loginList.setAttribute("count", "1234");
await SimpleTest.promiseWaitForCondition(() => loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
"Wait for text to get localized");
ok(loginList.shadowRoot.querySelector(".count").textContent.includes("1234"),
"The count attribute should be inherited by the .count span");
});
</script>
</body>
</html>

View File

@ -346,7 +346,7 @@
<button id="identity-popup-breakageReportView-submit"
default="true"
label="&contentBlocking.breakageReportView.sendReport.label;"
oncommand="ContentBlocking.submitBreakageReport();"/>
oncommand="ContentBlocking.onSubmitBreakageReportClicked();"/>
</vbox>
</panelview>
</panelmultiview>

View File

@ -25,19 +25,21 @@
</hbox>
<hbox id="protections-popup-tp-switch-section" class="identity-popup-section">
<vbox id="protections-popup-tp-switch-label-box" flex="1">
<label id="protections-popup-tp-switch-on-header"
<vbox class="protections-popup-tp-switch-label-box" flex="1">
<label class="protections-popup-tp-switch-on-header"
hidden="true">Tracking protection is ON for this site</label>
<label id="protections-popup-tp-switch-off-header"
<label class="protections-popup-tp-switch-off-header"
hidden="true">Tracking protection is OFF for this site</label>
<label id="protections-popup-tp-switch-breakage-link"
class="text-link"
onclick="gProtectionsHandler.showSiteNotWorkingView();"
hidden="true">Site not working?</label>
</vbox>
<vbox id="protections-popup-tp-switch-box">
<vbox class="protections-popup-tp-switch-box">
<toolbarbutton id="protections-popup-tp-switch"
enabled="false"
oncommand="gProtectionsHandler.onTPSwitchCommand();" />
class="protections-popup-tp-switch"
enabled="false"
oncommand="gProtectionsHandler.onTPSwitchCommand();" />
</vbox>
</hbox>
@ -59,5 +61,77 @@
href="about:protections">Show Full Report</label>
</hbox>
</panelview>
<!-- Site Not Working? SubView -->
<panelview id="protections-popup-siteNotWorkingView"
title="Site Not Working?"
descriptionheightworkaround="true"
flex="1">
<hbox id="protections-popup-siteNotWorkingView-header">
<vbox class="protections-popup-tp-switch-label-box" flex="1">
<label class="protections-popup-tp-switch-on-header"
hidden="true">Tracking protection is ON for this site</label>
<label class="protections-popup-tp-switch-off-header"
hidden="true">Tracking protection is OFF for this site</label>
</vbox>
<vbox class="protections-popup-tp-switch-box">
<toolbarbutton id="protections-popup-siteNotWorking-tp-switch"
class="protections-popup-tp-switch"
enabled="false"
oncommand="gProtectionsHandler.onTPSwitchCommand();" />
</vbox>
</hbox>
<vbox id="protections-popup-siteNotWorkingView-body" flex="1">
<label>Turn off Tracking Protection if you're having issues with:</label>
<label>
<html:ul id="protections-popup-siteNotWorkingView-body-issue-list">
<html:li>Log in fields</html:li>
<html:li>Forms</html:li>
<html:li>Payments</html:li>
<html:li>Comments</html:li>
<html:li>Videos</html:li>
</html:ul>
</label>
</vbox>
<hbox id="protections-popup-siteNotWorkingView-footer"
class="panel-footer">
<label id="protections-popup-siteNotWorkingView-siteStillBroken" flex="1">Site Still Broken?</label>
<label id="protections-popup-siteNotWorkingView-sendReport"
onclick="gProtectionsHandler.showSendReportView();"
class="text-link">Send Report</label>
</hbox>
</panelview>
<!-- Send Report SubView -->
<panelview id="protections-popup-sendReportView"
title="Send Report"
descriptionheightworkaround="true">
<vbox id="protections-popup-sendReportView-heading">
<description>&contentBlocking.breakageReportView2.description;</description>
<label id="protections-popup-sendReportView-learn-more"
is="text-link">&contentBlocking.breakageReportView.learnMore;</label>
</vbox>
<vbox id="protections-popup-sendReportView-body" class="panel-view-body-unscrollable">
<vbox class="protections-popup-sendReportView-collection-section">
<label>&contentBlocking.breakageReportView.collection.url.label;</label>
<html:input readonly="readonly" id="protections-popup-sendReportView-collection-url"/>
</vbox>
<vbox class="protections-popup-sendReportView-collection-section">
<label>&contentBlocking.breakageReportView.collection.comments.label;</label>
<html:textarea id="protections-popup-sendReportView-collection-comments"/>
</vbox>
</vbox>
<vbox id="protections-popup-sendReportView-footer"
class="panel-footer">
<button id="protections-popup-sendReportView-cancel"
label="&contentBlocking.breakageReportView.cancel.label;"
oncommand="gProtectionsHandler._protectionsPopupMultiView.goBack();"/>
<button id="protections-popup-sendReportView-submit"
default="true"
label="&contentBlocking.breakageReportView.sendReport.label;"
oncommand="gProtectionsHandler.onSendReportClicked();"/>
</vbox>
</panelview>
</panelmultiview>
</panel>

View File

@ -198,6 +198,14 @@
["tabs"]
]
},
"topSites": {
"url": "chrome://extensions/content/parent/ext-topSites.js",
"schema": "chrome://extensions/content/schemas/top_sites.json",
"scopes": ["addon_parent"],
"paths": [
["topSites"]
]
},
"urlbar": {
"url": "chrome://browser/content/parent/ext-urlbar.js",
"schema": "chrome://browser/content/schemas/urlbar.json",

View File

@ -19,7 +19,7 @@ const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
// Automated tests ensure packaged locales are in this list. Copied output of:
// https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id is it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mk mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr trs uk ur uz vi zh-CN zh-TW".split(" ");
const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id is it ja ka kab kk km kn ko lij lo lt ltg lv mk mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr trs uk ur uz vi zh-CN zh-TW".split(" ");
const ABOUT_URL = "about:newtab";
const BASE_URL = "resource://activity-stream/";

View File

@ -120,9 +120,11 @@ class UrlbarView {
* may be showing stale results.
*/
get visibleItemCount() {
return Array.reduce(this._rows.children, (sum, r) => {
return sum + Number(this._isRowVisible(r));
}, 0);
let sum = 0;
for (let row of this._rows.children) {
sum += Number(this._isRowVisible(row));
}
return sum;
}
/**

View File

@ -289,19 +289,19 @@ add_task(async function test_adaptive_mouse() {
await PlacesUtils.history.clear();
await bumpScore(url1, "site", {visits: 3, picks: 3}, true);
await bumpScore(url2, "site", {visits: 3, picks: 1}, true);
await promiseAutocompleteResultPopup("");
let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
await promiseAutocompleteResultPopup("si");
let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
Assert.equal(result.url, url1, "Check first result");
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
Assert.equal(result.url, url2, "Check second result");
info("Same visit count, different picks, invert");
await PlacesUtils.history.clear();
await bumpScore(url1, "site", {visits: 3, picks: 1}, true);
await bumpScore(url2, "site", {visits: 3, picks: 3}, true);
await promiseAutocompleteResultPopup("");
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
Assert.equal(result.url, url2, "Check first result");
await promiseAutocompleteResultPopup("si");
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
Assert.equal(result.url, url2, "Check first result");
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
Assert.equal(result.url, url1, "Check second result");
});

View File

@ -9,12 +9,9 @@
display: none;
}
#identity-popup {
--identity-popup-width: 33rem;
}
#identity-popup,
#protections-popup {
--protections-popup-width: 33rem;
--popup-width: 33rem;
}
/* This is used by screenshots tests to hide intermittently different
@ -78,13 +75,13 @@
}
#identity-popup-mainView {
min-width: var(--identity-popup-width);
max-width: var(--identity-popup-width);
min-width: var(--popup-width);
max-width: var(--popup-width);
}
#protections-popup-mainView {
min-width: var(--protections-popup-width);
max-width: var(--protections-popup-width);
min-width: var(--popup-width);
max-width: var(--popup-width);
}
#protections-popup[toast] #protections-popup-mainView > :not(#protections-popup-mainView-panel-header),
@ -192,6 +189,7 @@
#identity-popup-trackersView > .panel-header,
#identity-popup-securityView > .panel-header,
#identity-popup-breakageReportView > .panel-header,
#protections-popup-sendReportView > .panel-header,
#identity-popup-content-blocking-report-breakage,
.identity-popup-content-blocking-category-label,
.identity-popup-content-blocking-category-state-label,
@ -210,7 +208,8 @@
}
#identity-popup-mainView-panel-header,
#protections-popup-mainView-panel-header {
#protections-popup-mainView-panel-header,
#protections-popup-siteNotWorkingView-footer {
padding: 4px 1em;
min-height: 40px;
-moz-box-pack: center;
@ -240,7 +239,13 @@
overflow-wrap: break-word;
/* This is needed for the overflow-wrap to work correctly.
* 33em is the panel width, panel-header has 1em padding on each side. */
max-width: calc(var(--identity-popup-width) - 2em);
max-width: calc(var(--popup-width) - 2em);
}
#protections-popup-mainView-panel-header-span,
#protections-popup-toast-panel-tp-on-desc,
#protections-popup-toast-panel-tp-off-desc {
max-width: calc(var(--popup-width) - 2em);
}
#identity-popup-permissions-content > description,
@ -263,7 +268,7 @@
/* This is needed for the overflow-wrap to work correctly.
* 1em + 2em + 24px is .identity-popup-security-content padding
* 33em is the panel width */
max-width: calc(var(--identity-popup-width) - 3rem - 24px);
max-width: calc(var(--popup-width) - 3rem - 24px);
}
.identity-popup-warning-gray {
@ -371,33 +376,41 @@ description#identity-popup-content-verifier,
/* CONTENT BLOCKING / TRACKING PROTECTION */
#identity-popup-breakageReportView-footer {
#identity-popup-breakageReportView-footer,
#protections-popup-sendReportView-footer {
display: flex;
}
#identity-popup-breakageReportView-footer > button {
#identity-popup-breakageReportView-footer > button,
#protections-popup-sendReportView-footer > button {
flex: 1;
}
#identity-popup-breakageReportView-heading,
#identity-popup-breakageReportView-body {
#identity-popup-breakageReportView-body,
#protections-popup-sendReportView-heading,
#protections-popup-sendReportView-body {
padding: 16px;
font-size: 110%;
}
.identity-popup-breakageReportView-collection-section {
.identity-popup-breakageReportView-collection-section,
.protections-popup-sendReportView-collection-section {
margin-bottom: 16px;
}
#identity-popup-breakageReportView-body {
#identity-popup-breakageReportView-body,
#protections-popup-sendReportView-body {
border-top: 1px solid var(--panel-separator-color);
}
#identity-popup-breakageReportView-collection-url {
#identity-popup-breakageReportView-collection-url,
#protections-popup-sendReportView-collection-url {
font: inherit;
}
#identity-popup-breakageReportView-collection-comments {
#identity-popup-breakageReportView-collection-comments,
#protections-popup-sendReportView-collection-comments {
height: 120px;
}
@ -545,8 +558,8 @@ description#identity-popup-content-verifier,
#identity-popup-trackersView-strict-info {
min-height: 40px;
/* Limit to full width - margin */
max-width: calc(var(--identity-popup-width) - 12px);
min-width: calc(var(--identity-popup-width) - 12px);
max-width: calc(var(--popup-width) - 12px);
min-width: calc(var(--popup-width) - 12px);
background-color: #45a1ff80;
margin: 6px;
text-align: center;
@ -566,7 +579,7 @@ description#identity-popup-content-verifier,
#identity-popup-trackersView-strict-info > label {
overflow-wrap: break-word;
/* Limit to full width - container margin - container padding - icon width - icon margin */
max-width: calc(var(--identity-popup-width) - 12px - 20px - 16px - 10px);
max-width: calc(var(--popup-width) - 12px - 20px - 16px - 10px);
}
/* Content Blocking categories */
@ -811,8 +824,8 @@ description#identity-popup-content-verifier,
display: none;
}
#protections-popup:not([hasException]) #protections-popup-tp-switch-on-header,
#protections-popup[hasException] #protections-popup-tp-switch-off-header,
#protections-popup:not([hasException]) .protections-popup-tp-switch-on-header,
#protections-popup[hasException] .protections-popup-tp-switch-off-header,
#protections-popup:not([hasException])[toast] #protections-popup-toast-panel-tp-on-desc,
#protections-popup[hasException][toast] #protections-popup-toast-panel-tp-off-desc {
display: unset;
@ -832,8 +845,9 @@ description#identity-popup-content-verifier,
);
}
#protections-popup-tp-switch-label-box,
#protections-popup-tp-switch-box {
#protections-popup-siteNotWorkingView-body,
.protections-popup-tp-switch-label-box,
.protections-popup-tp-switch-box {
padding: 4px 1em;
min-height: 40px;
-moz-box-pack: center;
@ -842,12 +856,12 @@ description#identity-popup-content-verifier,
position: relative;
}
#protections-popup-tp-switch-on-header,
#protections-popup-tp-switch-off-header {
.protections-popup-tp-switch-on-header,
.protections-popup-tp-switch-off-header {
font-weight: 600;
}
#protections-popup-tp-switch {
.protections-popup-tp-switch {
-moz-appearance: none;
box-sizing: border-box;
min-width: 26px;
@ -857,12 +871,12 @@ description#identity-popup-content-verifier,
margin-top: 4px;
margin-bottom: 4px;
margin-inline-start: 1px;
margin-inline-end: 7px;
padding: 2px;
padding-inline-end: 0;
transition: padding .2s ease;
}
#protections-popup-tp-switch::before {
.protections-popup-tp-switch::before {
position: relative;
display: block;
content: "";
@ -872,23 +886,31 @@ description#identity-popup-content-verifier,
background: white;
}
#protections-popup-tp-switch[enabled] {
.protections-popup-tp-switch[enabled] {
background-color: #0a84ff;
border: 1px solid #0a84ff;
/* Push the toggle to the right. */
padding-inline-start: 12px;
}
#protections-popup-tp-switch:hover,
#protections-popup-tp-switch:-moz-focusring {
.protections-popup-tp-switch:hover,
.protections-popup-tp-switch:-moz-focusring {
border: 1px solid var(--panel-separator-color);
}
#protections-popup-tp-switch[enabled=true]:hover,
#protections-popup-tp-switch[enabled=true]:-moz-focusring {
.protections-popup-tp-switch[enabled=true]:hover,
.protections-popup-tp-switch[enabled=true]:-moz-focusring {
background-color: #45a1ff;
}
#protections-popup-siteNotWorkingView-body-issue-list {
padding-inline-start: 1em;
}
#protections-popup-siteNotWorkingView-footer {
border-top: 1px solid var(--panel-separator-color);
}
#protections-popup-settings-section {
padding: 4px;
-moz-context-properties: fill, fill-opacity;

View File

@ -204,7 +204,7 @@ button {
height: 12px;
vertical-align: 0;
margin-inline-end: 8px;
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
background-repeat: no-repeat;
background-size: contain;
}

View File

@ -52,8 +52,9 @@ AccessibilityView.prototype = {
* @param {JSON} supports a collection of flags indicating which accessibility
* panel features are supported by the current serverside
* version.
* @param {Array} fluentBundles array of FluentBundles elements for localization
*/
async initialize(accessibility, walker, supports) {
async initialize(accessibility, walker, supports, fluentBundles) {
// Make sure state is reset every time accessibility panel is initialized.
await this.store.dispatch(reset(accessibility, supports));
const container = document.getElementById("content");
@ -63,7 +64,7 @@ AccessibilityView.prototype = {
return;
}
const mainFrame = MainFrame({ accessibility, walker });
const mainFrame = MainFrame({ accessibility, walker, fluentBundles });
// Render top level component
const provider = createElement(Provider, { store: this.store }, mainFrame);
this.mainFrame = ReactDOM.render(provider, container);

View File

@ -28,6 +28,7 @@
--accessible-label-color: var(--grey-60);
/* Similarly to webconsole, add more padding before the toolbar group. */
--separator-inline-margin: 5px;
--accessibility-code-background: var(--grey-20);
}
:root.theme-dark {
@ -42,6 +43,7 @@
--accessible-label-background-color: var(--grey-80);
--accessible-label-border-color: var(--grey-50);
--accessible-label-color: var(--grey-40);
--accessibility-code-background: var(--grey-70);
}
/* General */
@ -660,8 +662,31 @@ body {
white-space: initial;
}
/* Color Contrast */
.accessibility-color-contrast-check,
/* Checks */
.accessibility-check code {
background-color: var(--accessibility-code-background);
border-radius: 2px;
box-decoration-break: clone;
padding: 0 4px;
}
.accessibility-text-label-check .icon {
display: inline;
-moz-context-properties: fill;
vertical-align: top;
margin-block-start: 2px;
margin-inline-end: 4px;
}
.accessibility-text-label-check .icon.fail {
fill: var(--theme-icon-error-color);
}
.accessibility-text-label-check .icon.WARNING {
fill: var(--theme-icon-warning-color);
}
.accessibility-check,
.accessibility-color-contrast {
position: relative;
display: flex;
@ -669,7 +694,7 @@ body {
height: inherit;
}
.accessibility-color-contrast-check {
.accessibility-check {
flex-direction: column;
padding: 4px var(--accessibility-horizontal-indent);
line-height: 20px;
@ -679,20 +704,21 @@ body {
align-items: baseline;
}
.accessibility-color-contrast-header {
.accessibility-check-header {
margin: 0;
font-weight: bold;
font-size: var(--accessibility-font-size);
line-height: var(--accessibility-toolbar-height);
}
.accessibility-color-contrast-annotation {
.accessibility-check-annotation {
display: inline;
margin: 0;
white-space: normal;
color: var(--accessible-label-color);
}
.accessibility-color-contrast-annotation .link {
.accessibility-check-annotation .link {
color: var(--accessibility-link-color);
cursor: pointer;
outline: 0;
@ -700,16 +726,16 @@ body {
font-style: normal;
}
.accessibility-color-contrast-annotation .link:hover:not(:focus) {
.accessibility-check-annotation .link:hover:not(:focus) {
text-decoration: underline;
}
.accessibility-color-contrast-annotation .link:focus:not(:active) {
.accessibility-check-annotation .link:focus:not(:active) {
box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus), 0 0 0 4px var(--accessibility-toolbar-focus-alpha30);
border-radius: 2px;
}
.accessibility-color-contrast-annotation .link:active {
.accessibility-check-annotation .link:active {
color: var(--accessibility-link-color-active);
text-decoration: underline;
}

View File

@ -12,6 +12,7 @@ const { div } = require("devtools/client/shared/vendor/react-dom-factories");
const List = createFactory(require("devtools/client/shared/components/List").List);
const ColorContrastCheck =
createFactory(require("./ColorContrastAccessibility").ColorContrastCheck);
const TextLabelCheck = createFactory(require("./TextLabelCheck"));
const { L10N } = require("../utils/l10n");
const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
@ -39,6 +40,10 @@ class Checks extends Component {
return ColorContrastCheck(contrastRatio);
}
[AUDIT_TYPE.TEXT_LABEL](textLabelCheck) {
return TextLabelCheck(textLabelCheck);
}
render() {
const { audit, labelledby } = this.props;
if (!audit) {

View File

@ -152,7 +152,7 @@ class ContrastAnnotationClass extends Component {
return (
LearnMoreLink(
{
className: "accessibility-color-contrast-annotation",
className: "accessibility-check-annotation",
href: A11Y_CONTRAST_LEARN_MORE_LINK,
learnMoreStringKey: "accessibility.learnMore",
l10n: L10N,
@ -178,10 +178,10 @@ class ColorContrastCheck extends Component {
return (
div({
role: "presentation",
className: "accessibility-color-contrast-check",
className: "accessibility-check",
},
h3({
className: "accessibility-color-contrast-header",
className: "accessibility-check-header",
}, L10N.getStr("accessibility.contrast.header")),
ColorContrastAccessibility(this.props),
!error && ContrastAnnotation(this.props)

View File

@ -12,6 +12,10 @@ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { reset } = require("../actions/ui");
// Localization
const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
// Constants
const { SIDEBAR_WIDTH, PORTRAIT_MODE_WIDTH } = require("../constants");
@ -31,6 +35,7 @@ class MainFrame extends Component {
static get propTypes() {
return {
accessibility: PropTypes.object.isRequired,
fluentBundles: PropTypes.array.isRequired,
walker: PropTypes.object.isRequired,
enabled: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
@ -91,7 +96,7 @@ class MainFrame extends Component {
* Render Accessibility panel content
*/
render() {
const { accessibility, walker, enabled, auditing } = this.props;
const { accessibility, walker, fluentBundles, enabled, auditing } = this.props;
if (!enabled) {
return Description({ accessibility });
@ -100,7 +105,7 @@ class MainFrame extends Component {
// Audit is currently running.
const isAuditing = auditing.length > 0;
return (
return LocalizationProvider({ messages: fluentBundles },
div({ className: "mainFrame", role: "presentation" },
Toolbar({ accessibility, walker }),
isAuditing && AuditProgressOverlay(),
@ -122,8 +127,10 @@ class MainFrame extends Component {
}, AccessibilityTree({ walker })),
endPanel: RightSidebar({ walker }),
vert: this.useLandscapeMode,
})),
));
})
),
)
);
}
}

View File

@ -0,0 +1,335 @@
/* 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/. */
"use strict";
// React
const { Component, createFactory } = require("devtools/client/shared/vendor/react");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const ReactDOM = require("devtools/client/shared/vendor/react-dom-factories");
const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const Localized = createFactory(FluentReact.Localized);
const { openDocLink } = require("devtools/client/shared/link");
const { A11Y_TEXT_LABEL_LINKS } = require("../constants");
const {
accessibility: {
AUDIT_TYPE: { TEXT_LABEL },
ISSUE_TYPE: {
[TEXT_LABEL]: {
AREA_NO_NAME_FROM_ALT,
DIALOG_NO_NAME,
DOCUMENT_NO_TITLE,
EMBED_NO_NAME,
FIGURE_NO_NAME,
FORM_FIELDSET_NO_NAME,
FORM_FIELDSET_NO_NAME_FROM_LEGEND,
FORM_NO_NAME,
FORM_NO_VISIBLE_NAME,
FORM_OPTGROUP_NO_NAME,
FORM_OPTGROUP_NO_NAME_FROM_LABEL,
FRAME_NO_NAME,
HEADING_NO_CONTENT,
HEADING_NO_NAME,
IFRAME_NO_NAME_FROM_TITLE,
IMAGE_NO_NAME,
INTERACTIVE_NO_NAME,
MATHML_GLYPH_NO_NAME,
TOOLBAR_NO_NAME,
},
},
SCORES: { BEST_PRACTICES, FAIL, WARNING },
},
} = require("devtools/shared/constants");
/**
* A map from text label issues to annotation component properties.
*/
const ISSUE_TO_ANNOTATION_MAP = {
[AREA_NO_NAME_FROM_ALT]: {
href: A11Y_TEXT_LABEL_LINKS.AREA_NO_NAME_FROM_ALT,
l10nId: "accessibility-text-label-issue-area",
args: {
get code() {
return ReactDOM.code({}, "alt");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <div> since we can't provide
// three args with the same name.
get div() {
return ReactDOM.code({}, "area");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <span> since we can't provide
// three args with the same name.
get span() {
return ReactDOM.code({}, "href");
},
},
},
[DIALOG_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.DIALOG_NO_NAME,
l10nId: "accessibility-text-label-issue-dialog",
},
[DOCUMENT_NO_TITLE]: {
href: A11Y_TEXT_LABEL_LINKS.DOCUMENT_NO_TITLE,
l10nId: "accessibility-text-label-issue-document-title",
args: {
get code() {
return ReactDOM.code({}, "title");
},
},
},
[EMBED_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.EMBED_NO_NAME,
l10nId: "accessibility-text-label-issue-embed",
},
[FIGURE_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FIGURE_NO_NAME,
l10nId: "accessibility-text-label-issue-figure",
},
[FORM_FIELDSET_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_FIELDSET_NO_NAME,
l10nId: "accessibility-text-label-issue-fieldset",
args: {
get code() {
return ReactDOM.code({}, "fieldset");
},
},
},
[FORM_FIELDSET_NO_NAME_FROM_LEGEND]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_FIELDSET_NO_NAME_FROM_LEGEND,
l10nId: "accessibility-text-label-issue-fieldset-legend",
args: {
get code() {
return ReactDOM.code({}, "legend");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <span> since we can't provide
// two args with the same name.
get span() {
return ReactDOM.code({}, "fieldset");
},
},
},
[FORM_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_NO_NAME,
l10nId: "accessibility-text-label-issue-form",
},
[FORM_NO_VISIBLE_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_NO_VISIBLE_NAME,
l10nId: "accessibility-text-label-issue-form-visible",
},
[FORM_OPTGROUP_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_OPTGROUP_NO_NAME,
l10nId: "accessibility-text-label-issue-optgroup",
args: {
get code() {
return ReactDOM.code({}, "optgroup");
},
},
},
[FORM_OPTGROUP_NO_NAME_FROM_LABEL]: {
href: A11Y_TEXT_LABEL_LINKS.FORM_OPTGROUP_NO_NAME_FROM_LABEL,
l10nId: "accessibility-text-label-issue-optgroup-label",
args: {
get code() {
return ReactDOM.code({}, "label");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <span> since we can't provide
// two args with the same name.
get span() {
return ReactDOM.code({}, "optgroup");
},
},
},
[FRAME_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.FRAME_NO_NAME,
l10nId: "accessibility-text-label-issue-frame",
args: {
get code() {
return ReactDOM.code({}, "frame");
},
},
},
[HEADING_NO_CONTENT]: {
href: A11Y_TEXT_LABEL_LINKS.HEADING_NO_CONTENT,
l10nId: "accessibility-text-label-issue-heading-content",
},
[HEADING_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.HEADING_NO_NAME,
l10nId: "accessibility-text-label-issue-heading",
},
[IFRAME_NO_NAME_FROM_TITLE]: {
href: A11Y_TEXT_LABEL_LINKS.IFRAME_NO_NAME_FROM_TITLE,
l10nId: "accessibility-text-label-issue-iframe",
args: {
get code() {
return ReactDOM.code({}, "title");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <span> since we can't provide
// two args with the same name.
get span() {
return ReactDOM.code({}, "iframe");
},
},
},
[IMAGE_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.IMAGE_NO_NAME,
l10nId: "accessibility-text-label-issue-image",
},
[INTERACTIVE_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.INTERACTIVE_NO_NAME,
l10nId: "accessibility-text-label-issue-interactive",
},
[MATHML_GLYPH_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.MATHML_GLYPH_NO_NAME,
l10nId: "accessibility-text-label-issue-glyph",
args: {
get code() {
return ReactDOM.code({}, "alt");
},
// Note: there is no way right now to use custom elements in privileged
// content. We have to use something like <span> since we can't provide
// two args with the same name.
get span() {
return ReactDOM.code({}, "mglyph");
},
},
},
[TOOLBAR_NO_NAME]: {
href: A11Y_TEXT_LABEL_LINKS.TOOLBAR_NO_NAME,
l10nId: "accessibility-text-label-issue-toolbar",
},
};
/**
* A map of accessibility scores to the text descriptions of check icons.
*/
const SCORE_TO_ICON_MAP = {
[BEST_PRACTICES]: {
l10nId: "accessibility-best-practices",
src: "chrome://devtools/skin/images/info.svg",
},
[FAIL]: {
l10nId: "accessibility-fail",
src: "chrome://devtools/skin/images/error.svg",
},
[WARNING]: {
l10nId: "accessibility-warning",
src: "chrome://devtools/skin/images/alert.svg",
},
};
/**
* Localized "Learn more" link that opens a new tab with relevant documentation.
*/
class LearnMoreClass extends Component {
static get propTypes() {
return {
href: PropTypes.string,
l10nId: PropTypes.string.isRequired,
onClick: PropTypes.func,
};
}
static get defaultProps() {
return {
href: "#",
l10nId: null,
onClick: LearnMoreClass.openDocOnClick,
};
}
static openDocOnClick(event) {
event.preventDefault();
openDocLink(event.target.href);
}
render() {
const { href, l10nId, onClick } = this.props;
const className = "link";
return Localized({ id: l10nId }, ReactDOM.a({ className, href, onClick }));
}
}
const LearnMore = createFactory(LearnMoreClass);
/**
* Renders icon with text description for the text label accessibility check.
*
* @param {Object}
* Options:
* - score: value from SCORES from "devtools/shared/constants"
*/
function Icon({ score }) {
const { l10nId, src } = SCORE_TO_ICON_MAP[score];
return Localized({ id: l10nId, attrs: { alt: true } },
ReactDOM.img({ src, className: `icon ${score}` })
);
}
/**
* Renders text description of the text label accessibility check.
*
* @param {Object}
* Options:
* - issue: value from ISSUE_TYPE[AUDIT_TYPE.TEXT_LABEL] from
* "devtools/shared/constants"
*/
function Annotation({ issue }) {
const { args, href, l10nId } = ISSUE_TO_ANNOTATION_MAP[issue];
return Localized({
id: l10nId,
a: LearnMore({ l10nId: "accessibility-learn-more", href }),
...args,
},
ReactDOM.p({ className: "accessibility-check-annotation" })
);
}
/**
* Component for rendering a check for text label accessibliity check failures,
* warnings and best practices suggestions association with a given
* accessibility object in the accessibility tree.
*/
class TextLabelCheck extends Component {
static get propTypes() {
return {
issue: PropTypes.string.isRequired,
score: PropTypes.string.isRequired,
};
}
render() {
const { issue, score } = this.props;
return ReactDOM.div({
role: "presentation",
className: "accessibility-check",
},
Localized({
id: "accessibility-text-label-header",
},
ReactDOM.h3({ className: "accessibility-check-header" })),
ReactDOM.div({
role: "presentation",
className: "accessibility-text-label-check",
},
Icon({ score }),
Annotation({ issue })
)
);
}
}
module.exports = TextLabelCheck;

View File

@ -22,5 +22,6 @@ DevToolsModules(
'MainFrame.js',
'RightSidebar.js',
'TextLabelBadge.js',
'TextLabelCheck.js',
'Toolbar.js'
)

View File

@ -3,7 +3,34 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
const {
accessibility: {
AUDIT_TYPE,
ISSUE_TYPE: {
[AUDIT_TYPE.TEXT_LABEL]: {
AREA_NO_NAME_FROM_ALT,
DIALOG_NO_NAME,
DOCUMENT_NO_TITLE,
EMBED_NO_NAME,
FIGURE_NO_NAME,
FORM_FIELDSET_NO_NAME,
FORM_FIELDSET_NO_NAME_FROM_LEGEND,
FORM_NO_NAME,
FORM_NO_VISIBLE_NAME,
FORM_OPTGROUP_NO_NAME,
FORM_OPTGROUP_NO_NAME_FROM_LABEL,
FRAME_NO_NAME,
HEADING_NO_CONTENT,
HEADING_NO_NAME,
IFRAME_NO_NAME_FROM_TITLE,
IMAGE_NO_NAME,
INTERACTIVE_NO_NAME,
MATHML_GLYPH_NO_NAME,
TOOLBAR_NO_NAME,
},
},
},
} = require("devtools/shared/constants");
// Used in accessible component for properties tree rendering.
exports.TREE_ROW_HEIGHT = 21;
@ -84,3 +111,41 @@ exports.A11Y_LEARN_MORE_LINK =
exports.A11Y_CONTRAST_LEARN_MORE_LINK =
"https://developer.mozilla.org/docs/Web/Accessibility/Understanding_WCAG/Perceivable/" +
"Color_contrast?utm_source=devtools&utm_medium=a11y-panel-checks-color-contrast";
const A11Y_TEXT_LABEL_LINK_BASE =
"https://developer.mozilla.org/docs/Web/Accessibility/Understanding_WCAG/Text_labels_and_names" +
"?utm_source=devtools&utm_medium=a11y-panel-checks-text-label";
const A11Y_TEXT_LABEL_LINK_IDS = {
[AREA_NO_NAME_FROM_ALT]:
"Use_alt_attribute_to_provide_a_name_for_areas_that_have_the_href_attribute",
[DIALOG_NO_NAME]: "Dialogs_should_have_a_name",
[DOCUMENT_NO_TITLE]: "Documents_must_have_a_title",
[EMBED_NO_NAME]: "Embedded_content_must_have_a_name",
[FIGURE_NO_NAME]: "Figures_with_optional_captions_should_have_a_name",
[FORM_FIELDSET_NO_NAME]: "Form_element_groups_must_have_a_name",
[FORM_FIELDSET_NO_NAME_FROM_LEGEND]:
"Use_legend_element_to_provide_a_name_for_form_element_groups",
[FORM_NO_NAME]: "Form_elements_must_have_a_name",
[FORM_NO_VISIBLE_NAME]: "Form_elements_should_have_a_visible_text_label",
[FORM_OPTGROUP_NO_NAME]: "Groupings_of_options_must_have_a_name",
[FORM_OPTGROUP_NO_NAME_FROM_LABEL]:
"Use_label_attribute_to_provide_a_name_for_groupings_of_options",
[FRAME_NO_NAME]: "Frames_must_have_a_name",
[HEADING_NO_NAME]: "Headings_must_have_a_name",
[HEADING_NO_CONTENT]: "Headings_must_have_visible_text_content",
[IFRAME_NO_NAME_FROM_TITLE]: "Use_title_attribute_to_describe_iframe_content",
[IMAGE_NO_NAME]: "Content_with_images_must_have_a_name",
[INTERACTIVE_NO_NAME]: "Interactive_elements_must_have_a_name",
[MATHML_GLYPH_NO_NAME]:
"Use_alt_attribute_to_provide_a_name_for_MathML_glyphs",
[TOOLBAR_NO_NAME]:
"Toolbars_must_have_a_name_when_there_is_more_than_one_toolbar",
};
const A11Y_TEXT_LABEL_LINKS = {};
for (const key in A11Y_TEXT_LABEL_LINK_IDS) {
A11Y_TEXT_LABEL_LINKS[key] =
`${A11Y_TEXT_LABEL_LINK_BASE}#${A11Y_TEXT_LABEL_LINK_IDS[key]}`;
}
exports.A11Y_TEXT_LABEL_LINKS = A11Y_TEXT_LABEL_LINKS;

View File

@ -3,6 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Services = require("Services");
const { L10nRegistry } = require("resource://gre/modules/L10nRegistry.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
const Telemetry = require("devtools/client/shared/telemetry");
@ -81,6 +84,8 @@ AccessibilityPanel.prototype = {
this.picker = new Picker(this);
}
this.fluentBundles = await this.createFluentBundles();
this.updateA11YServiceDurationTimer();
this.front.on("init", this.updateA11YServiceDurationTimer);
this.front.on("shutdown", this.updateA11YServiceDurationTimer);
@ -94,6 +99,25 @@ AccessibilityPanel.prototype = {
return this._opening;
},
/**
* Retrieve message contexts for the current locales, and return them as an
* array of FluentBundles elements.
*/
async createFluentBundles() {
const locales = Services.locale.appLocalesAsBCP47;
const generator =
L10nRegistry.generateBundles(locales, ["devtools/accessibility.ftl"]);
// Return value of generateBundles is a generator and should be converted to
// a sync iterable before using it with React.
const contexts = [];
for await (const message of generator) {
contexts.push(message);
}
return contexts;
},
onNewAccessibleFrontSelected(selected) {
this.emit("new-accessible-front-selected", selected);
},
@ -133,7 +157,8 @@ AccessibilityPanel.prototype = {
}
// Alright reset the flag we are about to refresh the panel.
this.shouldRefresh = false;
this.postContentMessage("initialize", this.front, this.walker, this.supports);
this.postContentMessage("initialize", this.front, this.walker, this.supports,
this.fluentBundles);
},
updateA11YServiceDurationTimer() {

View File

@ -0,0 +1,9 @@
/* 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/. */
"use strict";
module.exports = {
"plugins": ["@babel/plugin-proposal-async-generator-functions"],
};

View File

@ -4,10 +4,10 @@ exports[`AuditController component: audit filter filtered contrast checks fail 1
exports[`AuditController component: audit filter filtered contrast checks fail range 1`] = `"<span></span>"`;
exports[`AuditController component: audit filter filtered contrast checks success 1`] = `null`;
exports[`AuditController component: audit filter filtered contrast checks success 1`] = `""`;
exports[`AuditController component: audit filter filtered no checks 1`] = `null`;
exports[`AuditController component: audit filter filtered no checks 1`] = `""`;
exports[`AuditController component: audit filter filtered unknown checks 1`] = `null`;
exports[`AuditController component: audit filter filtered unknown checks 1`] = `""`;
exports[`AuditController component: audit filter not filtered 1`] = `"<span></span>"`;

View File

@ -10,4 +10,4 @@ exports[`AuditProgressOverlay component: render auditing progress 2`] = `"<span
exports[`AuditProgressOverlay component: render auditing progress 3`] = `"<span id=\\"audit-progress-container\\">accessibility.progress.progressbar<progress max=\\"100\\" value=\\"75\\" class=\\"audit-progress-progressbar\\" aria-labelledby=\\"audit-progress-container\\"></progress></span>"`;
exports[`AuditProgressOverlay component: render not auditing 1`] = `null`;
exports[`AuditProgressOverlay component: render not auditing 1`] = `""`;

View File

@ -6,10 +6,10 @@ exports[`Badges component: contrast ratio fail render 1`] = `"<span class=\\"bad
exports[`Badges component: contrast ratio success render 1`] = `"<span class=\\"badges\\" role=\\"group\\" aria-label=\\"accessibility.badges\\"></span>"`;
exports[`Badges component: empty checks render 1`] = `null`;
exports[`Badges component: empty checks render 1`] = `""`;
exports[`Badges component: no props render 1`] = `null`;
exports[`Badges component: no props render 1`] = `""`;
exports[`Badges component: null checks render 1`] = `null`;
exports[`Badges component: null checks render 1`] = `""`;
exports[`Badges component: unsupported checks render 1`] = `null`;
exports[`Badges component: unsupported checks render 1`] = `""`;

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TextLabelCheck component: BEST_PRACTICES render 1`] = `"<div role=\\"presentation\\" class=\\"accessibility-check\\"><h3 class=\\"accessibility-check-header\\"></h3><div role=\\"presentation\\" class=\\"accessibility-text-label-check\\"><img src=\\"chrome://devtools/skin/images/info.svg\\" class=\\"icon BEST_PRACTICES\\"><p class=\\"accessibility-check-annotation\\"></p></div></div>"`;
exports[`TextLabelCheck component: WARNING render 1`] = `"<div role=\\"presentation\\" class=\\"accessibility-check\\"><h3 class=\\"accessibility-check-header\\"></h3><div role=\\"presentation\\" class=\\"accessibility-text-label-check\\"><img src=\\"chrome://devtools/skin/images/alert.svg\\" class=\\"icon WARNING\\"><p class=\\"accessibility-check-annotation\\"></p></div></div>"`;
exports[`TextLabelCheck component: fail render 1`] = `"<div role=\\"presentation\\" class=\\"accessibility-check\\"><h3 class=\\"accessibility-check-header\\"></h3><div role=\\"presentation\\" class=\\"accessibility-text-label-check\\"><img src=\\"chrome://devtools/skin/images/error.svg\\" class=\\"icon fail\\"><p class=\\"accessibility-check-annotation\\"></p></div></div>"`;

View File

@ -0,0 +1,69 @@
/* 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/. */
"use strict";
const { mount } = require("enzyme");
const { createFactory } = require("devtools/client/shared/vendor/react");
const TextLabelCheckClass = require("devtools/client/accessibility/components/TextLabelCheck");
const TextLabelCheck = createFactory(TextLabelCheckClass);
const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
const {
accessibility: {
AUDIT_TYPE: { TEXT_LABEL },
ISSUE_TYPE: {
[TEXT_LABEL]: {
AREA_NO_NAME_FROM_ALT,
DIALOG_NO_NAME,
FORM_NO_VISIBLE_NAME,
},
},
SCORES: { BEST_PRACTICES, FAIL, WARNING },
},
} = require("devtools/shared/constants");
function testTextLabelCheck(wrapper, props) {
expect(wrapper.html()).toMatchSnapshot();
expect(wrapper.children().length).toBe(1);
const container = wrapper.childAt(0);
expect(container.hasClass("accessibility-check")).toBe(true);
expect(container.prop("role")).toBe("presentation");
expect(wrapper.props()).toMatchObject(props);
const localized = wrapper.find(FluentReact.Localized);
expect(localized.length).toBe(3);
const heading = localized.at(0).childAt(0);
expect(heading.type()).toBe("h3");
expect(heading.hasClass("accessibility-check-header")).toBe(true);
const icon = localized.at(1).childAt(0);
expect(icon.type()).toBe("img");
expect(icon.hasClass(props.score === FAIL ? "fail" : props.score)).toBe(true);
const annotation = localized.at(2).childAt(0);
expect(annotation.type()).toBe("p");
expect(annotation.hasClass("accessibility-check-annotation")).toBe(true);
}
describe("TextLabelCheck component:", () => {
const testProps = [
{ issue: AREA_NO_NAME_FROM_ALT, score: FAIL },
{ issue: FORM_NO_VISIBLE_NAME, score: WARNING },
{ issue: DIALOG_NO_NAME, score: BEST_PRACTICES },
];
for (const props of testProps) {
it(`${props.score} render`, () => {
const wrapper = mount(LocalizationProvider({ messages: []},
TextLabelCheck(props)));
const textLabelCheck = wrapper.find(TextLabelCheckClass);
testTextLabelCheck(textLabelCheck, props);
});
}
});

View File

@ -10,12 +10,13 @@
"test-ci": "jest --json"
},
"dependencies": {
"jest": "^23.0.0",
"@babel/plugin-proposal-async-generator-functions": "^7.2.0",
"jest": "^24.6.0",
"react-test-renderer": "16.4.1",
"react": "16.4.1",
"react-dom": "16.4.1",
"enzyme": "^3.3.0",
"enzyme-to-json": "3.3.4",
"enzyme-adapter-react-16": "^1.1.1"
"enzyme": "^3.9.0",
"enzyme-to-json": "^3.3.5",
"enzyme-adapter-react-16": "^1.13.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,13 @@ window._snapshots = {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
"className": "accessibility-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
"className": "accessibility-check-header",
},
"children": [
"Color and Contrast",
@ -45,13 +45,13 @@ window._snapshots = {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
"className": "accessibility-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
"className": "accessibility-check-header",
},
"children": [
"Color and Contrast",
@ -83,7 +83,7 @@ window._snapshots = {
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
"className": "accessibility-check-annotation",
},
"children": [
"Does not meet WCAG standards for accessible text. ",
@ -110,13 +110,13 @@ window._snapshots = {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
"className": "accessibility-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
"className": "accessibility-check-header",
},
"children": [
"Color and Contrast",
@ -170,7 +170,7 @@ window._snapshots = {
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
"className": "accessibility-check-annotation",
},
"children": [
"Does not meet WCAG standards for accessible text. ",
@ -197,13 +197,13 @@ window._snapshots = {
"type": "div",
"props": {
"role": "presentation",
"className": "accessibility-color-contrast-check",
"className": "accessibility-check",
},
"children": [
{
"type": "h3",
"props": {
"className": "accessibility-color-contrast-header",
"className": "accessibility-check-header",
},
"children": [
"Color and Contrast",
@ -246,7 +246,7 @@ window._snapshots = {
{
"type": "p",
"props": {
"className": "accessibility-color-contrast-annotation",
"className": "accessibility-check-annotation",
},
"children": [
"Meets WCAG AA standards for accessible text. ",

View File

@ -69,7 +69,12 @@ devtools.jar:
skin/images/accessibility.svg (themes/images/accessibility.svg)
skin/images/add.svg (themes/images/add.svg)
skin/images/alert.svg (themes/images/alert.svg)
skin/images/alert-small.svg (themes/images/alert-small.svg)
skin/images/alert-tiny.svg (themes/images/alert-tiny.svg)
skin/images/error.svg (themes/images/error.svg)
skin/images/error-small.svg (themes/images/error-small.svg)
skin/images/info.svg (themes/images/info.svg)
skin/images/info-small.svg (themes/images/info-small.svg)
skin/images/arrow.svg (themes/images/arrow.svg)
skin/images/arrow-big.svg (themes/images/arrow-big.svg)
skin/images/arrowhead-left.svg (themes/images/arrowhead-left.svg)
@ -122,8 +127,6 @@ devtools.jar:
skin/images/rules-view-print-simulation.svg (themes/images/rules-view-print-simulation.svg)
skin/markup.css (themes/markup.css)
skin/webconsole.css (themes/webconsole.css)
skin/images/webconsole/error.svg (themes/images/webconsole/error.svg)
skin/images/webconsole/info.svg (themes/images/webconsole/info.svg)
skin/images/webconsole/input.svg (themes/images/webconsole/input.svg)
skin/images/webconsole/navigation.svg (themes/images/webconsole/navigation.svg)
skin/images/webconsole/return.svg (themes/images/webconsole/return.svg)

View File

@ -0,0 +1,62 @@
# 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/.
### These strings are used inside the Accessibility panel.
accessibility-learn-more = Learn more
accessibility-text-label-header = Text Labels and Names
## Text entries that are used as text alternative for icons that depict accessibility isses.
accessibility-warning =
.alt = Warning
accessibility-fail =
.alt = Error
accessibility-best-practices =
.alt = Best Practices
## Text entries for a paragraph used in the accessibility panel sidebar's checks section
## that describe that currently selected accessible object has an accessibility issue
## with its text label or accessible name.
accessibility-text-label-issue-area = Use <code>alt</code> attribute to label <div>area</div> elements that have the <span>href</span> attribute. <a>Learn more</a>
accessibility-text-label-issue-dialog = Dialogs should be labeled. <a>Learn more</a>
accessibility-text-label-issue-document-title = Documents must have a <code>title</code>. <a>Learn more</a>
accessibility-text-label-issue-embed = Embedded content must be labeled. <a>Learn more</a>
accessibility-text-label-issue-figure = Figures with optional captions should be labeled. <a>Learn more</a>
accessibility-text-label-issue-fieldset = <code>fieldset</code> elements must be labeled. <a>Learn more</a>
accessibility-text-label-issue-fieldset-legend = Use <code>legend</code> element to label <span>fieldset</span> elements. <a>Learn more</a>
accessibility-text-label-issue-form = Form elements must be labeled. <a>Learn more</a>
accessibility-text-label-issue-form-visible = Form elements should have a visible text label. <a>Learn more</a>
accessibility-text-label-issue-frame = <code>frame</code> elements must be labeled. <a>Learn more</a>
accessibility-text-label-issue-glyph = Use <code>alt</code> attribute to label <span>mglyph</span> elements. <a>Learn more</a>
accessibility-text-label-issue-heading = Headings must be labeled. <a>Learn more</a>
accessibility-text-label-issue-heading-content = Headings should have visible text content. <a>Learn more</a>
accessibility-text-label-issue-iframe = Use <code>title</code> attribute to describe <span>iframe</span> content. <a>Learn more</a>
accessibility-text-label-issue-image = Content with images must be labeled. <a>Learn more</a>
accessibility-text-label-issue-interactive = Interactive elements must be labeled. <a>Learn more</a>
accessibility-text-label-issue-optgroup = <code>optgroup</code> elements must be labeled. <a>Learn more</a>
accessibility-text-label-issue-optgroup-label = Use <code>label</code> attribute to label <span>optgroup</span> elements. <a>Learn more</a>
accessibility-text-label-issue-toolbar = Toolbars must be labeled when there is more than one toolbar. <a>Learn more</a>

View File

@ -475,7 +475,7 @@
height: 12px;
vertical-align: -1px;
margin-inline-start: 5px;
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
background-size: cover;
-moz-context-properties: fill;
fill: var(--yellow-60);

View File

@ -282,7 +282,14 @@ pref("devtools.webconsole.sidebarToggle", false);
// Enable CodeMirror in the JsTerm
pref("devtools.webconsole.jsterm.codeMirror", true);
// Enable editor mode in the console.
// Enable editor mode in the console in Nightly builds.
#if defined(NIGHTLY_BUILD)
pref("devtools.webconsole.features.editor", true);
#else
pref("devtools.webconsole.features.editor", false);
#endif
// Saved editor mode state in the console.
pref("devtools.webconsole.input.editor", false);
// Disable the new performance recording panel by default

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12">
<path fill="context-fill" d="M6 0a1 1 0 0 1 .89.54l5 9.6A1 1 0 0 1 11 11.6H1a1 1 0 0 1-.89-1.46l5-9.6A1 1 0 0 1 6 0zm-.25 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5zM7 3.7a1 1 0 1 0-2 0v2.6a1 1 0 1 0 2 0V3.7z" />
</svg>

After

Width:  |  Height:  |  Size: 570 B

View File

@ -1,6 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12" height="12">
<path fill="context-fill" d="M6 0a1 1 0 0 1 .89.54l5 9.6A1 1 0 0 1 11 11.6H1a1 1 0 0 1-.89-1.46l5-9.6A1 1 0 0 1 6 0zm-.25 8a.75.75 0 0 0-.75.75v.5c0 .41.34.75.75.75h.5c.41 0 .75-.34.75-.75v-.5A.75.75 0 0 0 6.25 8h-.5zM7 3.7a1 1 0 1 0-2 0v2.6a1 1 0 1 0 2 0V3.7z" />
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M14.742 12.106L9.789 2.2a2 2 0 0 0-3.578 0l-4.953 9.91A2 2 0 0 0 3.047 15h9.905a2 2 0 0 0 1.79-2.894zM7 5a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0zm1 8.25A1.25 1.25 0 1 1 9.25 12 1.25 1.25 0 0 1 8 13.25z" />
</svg>

Before

Width:  |  Height:  |  Size: 570 B

After

Width:  |  Height:  |  Size: 531 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
<path fill="context-fill" fill-rule="evenodd" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm0-3.429a1.143 1.143 0 1 0 0-2.285 1.143 1.143 0 0 0 0 2.285zm0-3.428c.631 0 1.143-.512 1.143-1.143V4.571a1.143 1.143 0 0 0-2.286 0V8c0 .631.512 1.143 1.143 1.143z"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

@ -0,0 +1,6 @@
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z" />
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@ -83,7 +83,7 @@
height: 12px;
max-height: 12px;
margin-inline-end: 5px;
background-image: url(chrome://devtools/skin/images/webconsole/info.svg);
background-image: url(chrome://devtools/skin/images/info-small.svg);
background-repeat: no-repeat;
background-size: contain;
-moz-context-properties: fill;
@ -91,7 +91,7 @@
}
.opt-icon.warning::before {
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
fill: var(--yellow-60);
}

View File

@ -580,7 +580,7 @@ html, body, #app, #memory-tool {
height: 12px;
max-height: 12px;
margin-inline-end: 5px;
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
background-repeat: no-repeat;
background-size: contain;
-moz-context-properties: fill;

View File

@ -775,7 +775,7 @@ menuitem.experimental-option::before {
max-height: 12px;
margin-top: 2px;
margin-inline-end: 5px;
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
background-repeat: no-repeat;
background-size: contain;
-moz-context-properties: fill;

View File

@ -371,14 +371,14 @@
}
.ruleview-warning {
background-image: url(chrome://devtools/skin/images/alert.svg);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
fill: var(--yellow-60);
}
.ruleview-unused-warning {
background-image: url(chrome://devtools/skin/images/webconsole/info.svg);
background-image: url(chrome://devtools/skin/images/info-small.svg);
background-color: var(--theme-sidebar-background);
fill: var(--theme-icon-dimmed-color);
fill: var(--theme-icon-dimmed-color);
}
.ruleview-unused-warning:hover {

View File

@ -50,6 +50,8 @@
--theme-icon-color: rgba(12, 12, 13, 0.8);
--theme-icon-dimmed-color: rgba(135, 135, 137, 0.9);
--theme-icon-checked-color: var(--blue-60);
--theme-icon-error-color: var(--red-60);
--theme-icon-warning-color: var(--yellow-65);
/* Text color */
--theme-comment: var(--grey-50);
@ -151,6 +153,8 @@
--theme-icon-color: rgba(249, 249, 250, 0.7);
--theme-icon-dimmed-color: rgba(147, 147, 149, 0.9);
--theme-icon-checked-color: var(--blue-30);
--theme-icon-error-color: var(--red-40);
--theme-icon-warning-color: var(--yellow-60);
/* Text color */
--theme-comment: var(--grey-45);

View File

@ -21,11 +21,9 @@
--console-error-background: hsl(345, 23%, 24%);
--console-error-border: hsl(345, 30%, 35%);
--console-error-color: var(--red-20);
--console-error-icon-color: var(--red-40);
--console-warning-background: hsl(42, 37%, 19%);
--console-warning-border: hsl(60, 30%, 26%);
--console-warning-color: hsl(43, 94%, 81%);
--console-warning-icon-color: var(--yellow-60);
--console-navigation-color: var(--theme-highlight-blue);
--console-navigation-border: var(--blue-60);
--console-indent-border-color: var(--theme-highlight-blue);
@ -45,11 +43,9 @@
--console-error-background: hsl(344, 73%, 97%);
--console-error-border: rgba(215, 0, 34, 0.12); /* Red 60 + opacity */
--console-error-color: var(--red-70);
--console-error-icon-color: var(--red-60);
--console-warning-background: hsl(54, 100%, 92%);
--console-warning-border: rgba(215, 182, 0, 0.28); /* Yellow 60 + opacity */
--console-warning-color: var(--yellow-80);
--console-warning-icon-color: var(--yellow-65);
--console-navigation-color: var(--theme-highlight-blue);
--console-navigation-border: var(--blue-30);
--console-indent-border-color: var(--theme-highlight-blue);
@ -280,17 +276,17 @@ a {
.message.info > .icon {
color: var(--theme-icon-color);
background-image: url(chrome://devtools/skin/images/webconsole/info.svg);
background-image: url(chrome://devtools/skin/images/info-small.svg);
}
.message.error > .icon {
color: var(--console-error-icon-color);
background-image: url(chrome://devtools/skin/images/webconsole/error.svg);
color: var(--theme-icon-error-color);
background-image: url(chrome://devtools/skin/images/error-small.svg);
}
.message.warn > .icon {
color: var(--console-warning-icon-color);
background-image: url(chrome://devtools/skin/images/alert.svg);
color: var(--theme-icon-warning-color);
background-image: url(chrome://devtools/skin/images/alert-small.svg);
}
.message.navigationMarker > .icon {

View File

@ -59,6 +59,7 @@ class App extends Component {
sidebarVisible: PropTypes.bool.isRequired,
filterBarDisplayMode:
PropTypes.oneOf([...Object.values(FILTERBAR_DISPLAY_MODES)]).isRequired,
editorFeatureEnabled: PropTypes.bool.isRequired,
};
}
@ -74,6 +75,7 @@ class App extends Component {
const {
dispatch,
webConsoleUI,
editorFeatureEnabled,
} = this.props;
if (
@ -85,10 +87,13 @@ class App extends Component {
event.stopPropagation();
}
if (event.key.toLowerCase() === "b" && (
isMacOS && event.metaKey ||
!isMacOS && event.ctrlKey
)) {
if (
editorFeatureEnabled &&
event.key.toLowerCase() === "b" && (
isMacOS && event.metaKey ||
!isMacOS && event.ctrlKey
)
) {
event.stopPropagation();
event.preventDefault();
dispatch(actions.editorToggle());

View File

@ -328,9 +328,11 @@ class FilterBar extends Component {
children.push(dom.div(
{
className: "devtools-toolbar split-console-close-button-wrapper",
key: "wrapper",
},
dom.button({
id: "split-console-close-button",
key: "split-console-close-button",
className: "devtools-button",
title: l10n.getStr("webconsole.closeSplitConsoleButton.tooltip"),
onClick: () => {

View File

@ -81,6 +81,7 @@ const prefs = {
JSTERM_CODE_MIRROR: "devtools.webconsole.jsterm.codeMirror",
AUTOCOMPLETE: "devtools.webconsole.input.autocomplete",
GROUP_WARNINGS: "devtools.webconsole.groupWarningMessages",
EDITOR: "devtools.webconsole.features.editor",
},
},
};

View File

@ -15,6 +15,7 @@ const PrefState = (overrides) => Object.freeze(Object.assign({
jstermCodeMirror: false,
groupWarnings: false,
historyCount: 50,
editor: false,
}, overrides));
function prefs(state = PrefState(), action) {

View File

@ -51,6 +51,7 @@ function configureStore(webConsoleUI, options = {}) {
const jstermCodeMirror = getBoolPref(PREFS.FEATURES.JSTERM_CODE_MIRROR);
const autocomplete = getBoolPref(PREFS.FEATURES.AUTOCOMPLETE);
const groupWarnings = getBoolPref(PREFS.FEATURES.GROUP_WARNINGS);
const editor = getBoolPref(PREFS.FEATURES.EDITOR);
const historyCount = getIntPref(PREFS.UI.INPUT_HISTORY_COUNT);
const initialState = {
@ -61,6 +62,7 @@ function configureStore(webConsoleUI, options = {}) {
autocomplete,
historyCount,
groupWarnings,
editor,
}),
filters: FilterState({
error: getBoolPref(PREFS.FILTER.ERROR),

View File

@ -28,6 +28,7 @@ pref("devtools.webconsole.groupWarningMessages", false);
pref("devtools.webconsole.input.editor", false);
pref("devtools.webconsole.input.autocomplete", true);
pref("devtools.browserconsole.contentMessages", true);
pref("devtools.webconsole.features.editor", true);
global.loader = {
lazyServiceGetter: () => {},

View File

@ -8,6 +8,7 @@
const TEST_URI = "data:text/html;charset=utf8,<p>Test editor";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);
await performTests();

View File

@ -10,6 +10,7 @@
const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 1519314";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
await pushPref("devtools.webconsole.input.editor", true);
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);

View File

@ -10,6 +10,7 @@
const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 1519313";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
await pushPref("devtools.webconsole.input.editor", true);
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);

View File

@ -10,6 +10,7 @@
const TEST_URI = "data:text/html;charset=utf-8,Test JsTerm editor line gutters";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
await pushPref("devtools.webconsole.input.editor", true);
const hud = await openNewTabAndConsole(TEST_URI);

View File

@ -10,6 +10,7 @@ const TEST_URI = "data:text/html;charset=utf-8,Test editor mode toggle keyboard
const EDITOR_PREF = "devtools.webconsole.input.editor";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
await pushPref("devtools.webconsole.input.editor", true);
// Run test with legacy JsTerm
info("Test legacy JsTerm");

View File

@ -8,6 +8,7 @@
const TEST_URI = "data:text/html;charset=utf8,<p>Test editor toolbar";
add_task(async function() {
await pushPref("devtools.webconsole.features.editor", true);
// Run test with legacy JsTerm
await pushPref("devtools.webconsole.jsterm.codeMirror", false);
await performTests();

View File

@ -358,6 +358,7 @@ class WebConsoleWrapper {
const jstermCodeMirror = prefs.jstermCodeMirror
&& !Services.appinfo.accessibilityEnabled;
const autocomplete = prefs.autocomplete;
const editorFeatureEnabled = prefs.editor;
this.prefsObservers = new Map();
this.prefsObservers.set(PREFS.UI.MESSAGE_TIMESTAMP, () => {
@ -381,6 +382,7 @@ class WebConsoleWrapper {
closeSplitConsole: this.closeSplitConsole.bind(this),
jstermCodeMirror,
autocomplete,
editorFeatureEnabled,
hideShowContentMessagesCheckbox: !webConsoleUI.isBrowserConsole,
});

View File

@ -39,6 +39,8 @@
--highlighter-marker-color: #000;
--grey-40: #b1b1b3;
--red-40: #ff3b6b;
--yellow-60: #d7b600;
}
/**
@ -737,6 +739,34 @@
margin-inline-start: 3px;
}
:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label:before {
display: inline-block;
width: 12px;
height: 12px;
content: "";
margin-inline-end: 4px;
vertical-align: -2px;
background-image: none;
background-position: center;
background-repeat: no-repeat;
-moz-context-properties: fill;
fill: currentColor;
}
:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.fail:before {
background-image: url(chrome://devtools/skin/images/error-small.svg);
fill: var(--red-40);
}
:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.WARNING:before {
background-image: url(chrome://devtools/skin/images/alert-small.svg);
fill: var(--yellow-60);
}
:-moz-native-anonymous .accessible-infobar-audit .accessible-text-label.BEST_PRACTICES:before {
background-image: url(chrome://devtools/skin/images/info-small.svg);
}
:-moz-native-anonymous .accessible-infobar-name:not(:empty) {
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;

View File

@ -9,13 +9,39 @@ const { getCurrentZoom, getViewportDimensions } = require("devtools/shared/layou
const { moveInfobar, createNode } = require("./markup");
const { truncateString } = require("devtools/shared/inspector/utils");
const { accessibility: { SCORES } } = require("devtools/shared/constants");
const STRINGS_URI = "devtools/shared/locales/accessibility.properties";
loader.lazyRequireGetter(this, "LocalizationHelper", "devtools/shared/l10n", true);
DevToolsUtils.defineLazyGetter(this, "L10N", () => new LocalizationHelper(STRINGS_URI));
const { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
const {
accessibility: {
AUDIT_TYPE,
ISSUE_TYPE: {
[AUDIT_TYPE.TEXT_LABEL]: {
AREA_NO_NAME_FROM_ALT,
DIALOG_NO_NAME,
DOCUMENT_NO_TITLE,
EMBED_NO_NAME,
FIGURE_NO_NAME,
FORM_FIELDSET_NO_NAME,
FORM_FIELDSET_NO_NAME_FROM_LEGEND,
FORM_NO_NAME,
FORM_NO_VISIBLE_NAME,
FORM_OPTGROUP_NO_NAME,
FORM_OPTGROUP_NO_NAME_FROM_LABEL,
FRAME_NO_NAME,
HEADING_NO_CONTENT,
HEADING_NO_NAME,
IFRAME_NO_NAME_FROM_TITLE,
IMAGE_NO_NAME,
INTERACTIVE_NO_NAME,
MATHML_GLYPH_NO_NAME,
TOOLBAR_NO_NAME,
},
},
SCORES,
},
} = require("devtools/shared/constants");
// Max string length for truncating accessible name values.
const MAX_STRING_LENGTH = 50;
@ -383,6 +409,7 @@ class Audit {
// object.
this.reports = [
new ContrastRatio(this),
new TextLabel(this),
];
}
@ -539,13 +566,13 @@ class ContrastRatio extends AuditReport {
/**
* Update contrast ratio score infobar markup.
* @param {Number}
* Contrast ratio for an accessible object being highlighted.
* @param {Object}
* Audit report for a given highlighted accessible.
* @return {Boolean}
* True if the contrast ratio markup was updated correctly and infobar audit
* block should be visible.
*/
update({ [AUDIT_TYPE.CONTRAST]: contrastRatio }) {
update(audit) {
const els = {};
for (const key of ["label", "min", "max", "error", "separator"]) {
const el = els[key] = this.getElement(`contrast-ratio-${key}`);
@ -559,6 +586,11 @@ class ContrastRatio extends AuditReport {
el.removeAttribute("style");
}
if (!audit) {
return false;
}
const contrastRatio = audit[AUDIT_TYPE.CONTRAST];
if (!contrastRatio) {
return false;
}
@ -592,6 +624,82 @@ class ContrastRatio extends AuditReport {
}
}
/**
* Text label audit report that is used to display a problem with text alternatives
* as part of the inforbar.
*/
class TextLabel extends AuditReport {
/**
* A map from text label issues to annotation component properties.
*/
static get ISSUE_TO_INFOBAR_LABEL_MAP() {
return {
[AREA_NO_NAME_FROM_ALT]: "accessibility.text.label.issue.area",
[DIALOG_NO_NAME]: "accessibility.text.label.issue.dialog",
[DOCUMENT_NO_TITLE]: "accessibility.text.label.issue.document.title",
[EMBED_NO_NAME]: "accessibility.text.label.issue.embed",
[FIGURE_NO_NAME]: "accessibility.text.label.issue.figure",
[FORM_FIELDSET_NO_NAME]: "accessibility.text.label.issue.fieldset",
[FORM_FIELDSET_NO_NAME_FROM_LEGEND]:
"accessibility.text.label.issue.fieldset.legend",
[FORM_NO_NAME]: "accessibility.text.label.issue.form",
[FORM_NO_VISIBLE_NAME]: "accessibility.text.label.issue.form.visible",
[FORM_OPTGROUP_NO_NAME]: "accessibility.text.label.issue.optgroup",
[FORM_OPTGROUP_NO_NAME_FROM_LABEL]: "accessibility.text.label.issue.optgroup.label",
[FRAME_NO_NAME]: "accessibility.text.label.issue.frame",
[HEADING_NO_CONTENT]: "accessibility.text.label.issue.heading.content",
[HEADING_NO_NAME]: "accessibility.text.label.issue.heading",
[IFRAME_NO_NAME_FROM_TITLE]: "accessibility.text.label.issue.iframe",
[IMAGE_NO_NAME]: "accessibility.text.label.issue.image",
[INTERACTIVE_NO_NAME]: "accessibility.text.label.issue.interactive",
[MATHML_GLYPH_NO_NAME]: "accessibility.text.label.issue.glyph",
[TOOLBAR_NO_NAME]: "accessibility.text.label.issue.toolbar",
};
}
buildMarkup(root) {
createNode(this.win, {
nodeType: "span",
parent: root,
attributes: {
"class": "text-label",
"id": "text-label",
},
prefix: this.prefix,
});
}
/**
* Update text label audit infobar markup.
* @param {Object}
* Audit report for a given highlighted accessible.
* @return {Boolean}
* True if the text label markup was updated correctly and infobar
* audit block should be visible.
*/
update(audit) {
const el = this.getElement("text-label");
el.setAttribute("hidden", true);
Object.values(SCORES).forEach(className => el.classList.remove(className));
if (!audit) {
return false;
}
const textLabelAudit = audit[AUDIT_TYPE.TEXT_LABEL];
if (!textLabelAudit) {
return false;
}
const { issue, score } = textLabelAudit;
this.setTextContent(el, L10N.getStr(TextLabel.ISSUE_TO_INFOBAR_LABEL_MAP[issue]));
el.classList.add(score);
el.removeAttribute("hidden");
return true;
}
}
/**
* A helper function that calculate accessible object bounds and positioning to
* be used for highlighting.

View File

@ -20,6 +20,8 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
--highlighter-bubble-arrow-size: 8px;
--grey-40: #b1b1b3;
--red-40: #ff3b6b;
--yellow-60: #d7b600;
}
.accessible-bounds {
@ -147,6 +149,34 @@ const ACCESSIBLE_BOUNDS_SHEET = "data:text/css;charset=utf-8," + encodeURICompon
border-inline-start: 1px solid #5a6169;
margin-inline-start: 6px;
padding-inline-start: 6px;
}
.accessible-infobar-audit .accessible-text-label:before {
display: inline-block;
width: 12px;
height: 12px;
content: "";
margin-inline-end: 4px;
vertical-align: -2px;
background-image: none;
background-position: center;
background-repeat: no-repeat;
-moz-context-properties: fill;
fill: currentColor;
}
.accessible-infobar-audit .accessible-text-label.fail:before {
background-image: url(chrome://devtools/skin/images/error-small.svg);
fill: var(--red-40);
}
.accessible-infobar-audit .accessible-text-label.WARNING:before {
background-image: url(chrome://devtools/skin/images/alert-small.svg);
fill: var(--yellow-60);
}
.accessible-infobar-audit .accessible-text-label.BEST_PRACTICES:before {
background-image: url(chrome://devtools/skin/images/info-small.svg);
}`);
/**

View File

@ -43,6 +43,7 @@ support-files =
[browser_accessibility_highlighter_infobar.js]
skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
[browser_accessibility_infobar_show.js]
[browser_accessibility_infobar_audit_text_label.js]
[browser_accessibility_node.js]
skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184
[browser_accessibility_node_audit.js]

View File

@ -0,0 +1,127 @@
/* 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/. */
"use strict";
// Checks for the AccessibleHighlighter's infobar component and its text label
// audit.
add_task(async function() {
await BrowserTestUtils.withNewTab({
gBrowser,
url: MAIN_DOMAIN + "doc_accessibility_infobar.html",
}, async function(browser) {
await ContentTask.spawn(browser, null, async function() {
const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
const { HighlighterEnvironment } = require("devtools/server/actors/highlighters");
const { AccessibleHighlighter } = require("devtools/server/actors/highlighters/accessible");
const { LocalizationHelper } = require("devtools/shared/l10n");
const L10N = new LocalizationHelper(
"devtools/shared/locales/accessibility.properties");
const {
accessibility: {
AUDIT_TYPE,
ISSUE_TYPE: {
[AUDIT_TYPE.TEXT_LABEL]: {
DIALOG_NO_NAME,
FORM_NO_VISIBLE_NAME,
TOOLBAR_NO_NAME,
},
},
SCORES: { BEST_PRACTICES, FAIL, WARNING },
},
} = require("devtools/shared/constants");
/**
* Checks for updated content for an infobar.
*
* @param {Object} infobar
* Accessible highlighter's infobar component.
* @param {Object} audit
* Audit information that is passed on highlighter show.
*/
function checkTextLabel(infobar, audit) {
const { issue, score } = audit || {};
let expected = "";
if (issue) {
const { ISSUE_TO_INFOBAR_LABEL_MAP } = infobar.audit.reports[1].constructor;
expected = L10N.getStr(ISSUE_TO_INFOBAR_LABEL_MAP[issue]);
}
is(infobar.getTextContent("text-label"), expected,
"infobar text label audit text content is correct");
if (score) {
ok(infobar.getElement("text-label").classList.contains(
score === FAIL ? "fail" : score));
}
}
// Start testing. First, create highlighter environment and initialize.
const env = new HighlighterEnvironment();
env.initFromWindow(content.window);
// Wait for loading highlighter environment content to complete before creating the
// highlighter.
await new Promise(resolve => {
const doc = env.document;
function onContentLoaded() {
if (doc.readyState === "interactive" || doc.readyState === "complete") {
resolve();
} else {
doc.addEventListener("DOMContentLoaded", onContentLoaded, { once: true });
}
}
onContentLoaded();
});
// Now, we can test the Infobar's audit content.
const node = content.document.createElement("div");
content.document.body.append(node);
const highlighter = new AccessibleHighlighter(env);
const infobar = highlighter.accessibleInfobar;
const bounds = {
x: 0,
y: 0,
w: 250,
h: 100,
};
const tests = [{
desc: "Infobar is shown with no text label audit content when no audit.",
}, {
desc: "Infobar is shown with no text label audit content when audit is null.",
audit: null,
}, {
desc: "Infobar is shown with no text label audit content when empty " +
"text label audit.",
audit: { [AUDIT_TYPE.TEXT_LABEL]: null },
}, {
desc: "Infobar is shown with text label audit content for an error.",
audit: { [AUDIT_TYPE.TEXT_LABEL]: { score: FAIL, issue: TOOLBAR_NO_NAME } },
}, {
desc: "Infobar is shown with text label audit content for a warning.",
audit: { [AUDIT_TYPE.TEXT_LABEL]: {
score: WARNING, issue: FORM_NO_VISIBLE_NAME,
}},
}, {
desc: "Infobar is shown with text label audit content for best practices.",
audit: { [AUDIT_TYPE.TEXT_LABEL]: {
score: BEST_PRACTICES, issue: DIALOG_NO_NAME,
}},
}];
for (const test of tests) {
const { desc, audit } = test;
info(desc);
highlighter.show(node, { ...bounds, audit });
checkTextLabel(infobar, audit && audit[AUDIT_TYPE.TEXT_LABEL]);
highlighter.hide();
}
});
});
});

View File

@ -19,3 +19,98 @@ accessibility.contrast.ratio.label=Contrast:
# contrast ratio description that also specifies that the color contrast criteria used is
# if for large text.
accessibility.contrast.ratio.label.large=Contrast (large text):
# LOCALIZATION NOTE (accessibility.text.label.issue.area): A title text that
# describes that currently selected accessible object for an <area> element must have
# its name provided via the alt attribute.
accessibility.text.label.issue.area = Use “alt” attribute to label “area” elements that have the “href” attribute.
# LOCALIZATION NOTE (accessibility.text.label.issue.dialog): A title text that
# describes that currently selected accessible object for a dialog should have a name
# provided.
accessibility.text.label.issue.dialog = Dialogs should be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.document.title): A title text that
# describes that currently selected accessible object for a document must have a name
# provided via title.
accessibility.text.label.issue.document.title = Documents must have a title.
# LOCALIZATION NOTE (accessibility.text.label.issue.embed): A title text that
# describes that currently selected accessible object for an <embed> must have a name
# provided.
accessibility.text.label.issue.embed = Embedded content must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.figure): A title text that
# describes that currently selected accessible object for a figure should have a name
# provided.
accessibility.text.label.issue.figure = Figures with optional captions should be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset): A title text that
# describes that currently selected accessible object for a <fieldset> must have a name
# provided.
accessibility.text.label.issue.fieldset = “fieldset” elements must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.fieldset.legend): A title text that
# describes that currently selected accessible object for a <fieldset> must have a name
# provided via <legend> element.
accessibility.text.label.issue.fieldset.legend = Use “legend” element to label “fieldset” elements.
# LOCALIZATION NOTE (accessibility.text.label.issue.form): A title text that
# describes that currently selected accessible object for a form element must have a name
# provided.
accessibility.text.label.issue.form = Form elements must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.form.visible): A title text that
# describes that currently selected accessible object for a form element should have a name
# provided via a visible label/element.
accessibility.text.label.issue.form.visible = Form elements should have a visible text label.
# LOCALIZATION NOTE (accessibility.text.label.issue.frame): A title text that
# describes that currently selected accessible object for a <frame> must have a name
# provided.
accessibility.text.label.issue.frame = “frame” elements must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.glyph): A title text that
# describes that currently selected accessible object for a <mglyph> must have a name
# provided via alt attribute.
accessibility.text.label.issue.glyph = Use “alt” attribute to label “mglyph” elements.
# LOCALIZATION NOTE (accessibility.text.label.issue.heading): A title text that
# describes that currently selected accessible object for a heading must have a name
# provided.
accessibility.text.label.issue.heading = Headings must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.heading.content): A title text that
# describes that currently selected accessible object for a heading must have visible
# content.
accessibility.text.label.issue.heading.content = Headings should have visible text content.
# LOCALIZATION NOTE (accessibility.text.label.issue.iframe): A title text that
# describes that currently selected accessible object for an <iframe> have a name
# provided via title attribute.
accessibility.text.label.issue.iframe = Use “title” attribute to describe “iframe” content.
# LOCALIZATION NOTE (accessibility.text.label.issue.image): A title text that
# describes that currently selected accessible object for graphical content must have a
# name provided.
accessibility.text.label.issue.image = Content with images must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.interactive): A title text that
# describes that currently selected accessible object for interactive element must have a
# name provided.
accessibility.text.label.issue.interactive = Interactive elements must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.optgroup): A title text that
# describes that currently selected accessible object for an <optgroup> must have a
# name provided.
accessibility.text.label.issue.optgroup = “optgroup” elements must be labeled.
# LOCALIZATION NOTE (accessibility.text.label.issue.optgroup.label): A title text that
# describes that currently selected accessible object for an <optgroup> must have a
# name provided via label attribute.
accessibility.text.label.issue.optgroup.label = Use “label” attribute to label “optgroup” elements.
# LOCALIZATION NOTE (accessibility.text.label.issue.toolbar): A title text that
# describes that currently selected accessible object for a toolbar must have a
# name provided when there is more than one toolbar in the document.
accessibility.text.label.issue.toolbar = Toolbars must be labeled when there is more than one toolbar.

View File

@ -1,3 +1,7 @@
/* 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/. */
"use strict";
const { extend } = require("devtools/shared/extend");

View File

@ -1,3 +1,7 @@
/* 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/. */
"use strict";
var { Request } = require("../Request");

View File

@ -1,3 +1,7 @@
/* 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/. */
"use strict";
var { settleAll } = require("devtools/shared/DevToolsUtils");

Some files were not shown because too many files have changed in this diff Show More