DevTools: Provide error message for Devices Settings Pane (ListWidget)
This change provides the infrastructure to give the user an error
message whenever they enter invalid input into one of the fields of the
ListWidget.Editor.
It also provides the specific error message for the Devices Settings
pane.
Screenshot of error message dialogue: https://imgur.com/a/wD4akTa
This work was originally part of https://chromium-review.googlesource.com/c/chromium/src/+/1562830
Bug: 963183
Change-Id: Ie984366f5f85880cacaaeaea69f5187c5313ac6d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1629757
Commit-Queue: Amanda Baker <[email protected]>
Reviewed-by: Joel Einbinder <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#673884}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: c346cec0ac5cf9dd2c32acdc44a16d241a9375dd
diff --git a/front_end/devices/DevicesView.js b/front_end/devices/DevicesView.js
index d8f444e..4ea877f 100644
--- a/front_end/devices/DevicesView.js
+++ b/front_end/devices/DevicesView.js
@@ -390,35 +390,38 @@
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @this {Devices.DevicesView.PortForwardingView}
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function portValidator(rule, index, input) {
+ let valid = true;
const value = input.value.trim();
const match = value.match(/^(\d+)$/);
- if (!match)
- return false;
- const port = parseInt(match[1], 10);
- if (port < 1024 || port > 65535)
- return false;
- for (let i = 0; i < this._portForwardingConfig.length; ++i) {
- if (i !== index && this._portForwardingConfig[i].port === value)
- return false;
+ if (!match) {
+ valid = false;
+ } else {
+ const port = parseInt(match[1], 10);
+ if (port < 1024 || port > 65535)
+ valid = false;
+ for (let i = 0; i < this._portForwardingConfig.length; ++i) {
+ if (i !== index && this._portForwardingConfig[i].port === value)
+ valid = false;
+ }
}
- return true;
+ return {valid};
}
/**
* @param {!Adb.PortForwardingRule} rule
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function addressValidator(rule, index, input) {
const match = input.value.trim().match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
if (!match)
- return false;
+ return {valid: false};
const port = parseInt(match[2], 10);
- return port <= 65535;
+ return {valid: port <= 65535};
}
}
};
diff --git a/front_end/emulation/DeviceModeModel.js b/front_end/emulation/DeviceModeModel.js
index d2e9793..27510aa 100644
--- a/front_end/emulation/DeviceModeModel.js
+++ b/front_end/emulation/DeviceModeModel.js
@@ -69,23 +69,67 @@
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
- static deviceSizeValidator(value) {
- if (/^[\d]+$/.test(value) && value >= Emulation.DeviceModeModel.MinDeviceSize &&
- value <= Emulation.DeviceModeModel.MaxDeviceSize)
- return true;
- return false;
+ static widthValidator(value) {
+ let valid = false;
+ let errorMessage;
+
+ if (!/^[\d]+$/.test(value))
+ errorMessage = ls`Width must be a number.`;
+ else if (value > Emulation.DeviceModeModel.MaxDeviceSize)
+ errorMessage = ls`Width must be less than or equal to ${Emulation.DeviceModeModel.MaxDeviceSize}.`;
+ else if (value < Emulation.DeviceModeModel.MinDeviceSize)
+ errorMessage = ls`Width must be greater than or equal to ${Emulation.DeviceModeModel.MinDeviceSize}.`;
+ else
+ valid = true;
+
+ return {valid, errorMessage};
}
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
- static deviceScaleFactorValidator(value) {
- if (!value || (/^[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= 0 && value <= 10))
- return true;
- return false;
+ static heightValidator(value) {
+ let valid = false;
+ let errorMessage;
+
+ if (!/^[\d]+$/.test(value))
+ errorMessage = ls`Height must be a number.`;
+ else if (value > Emulation.DeviceModeModel.MaxDeviceSize)
+ errorMessage = ls`Height must be less than or equal to ${Emulation.DeviceModeModel.MaxDeviceSize}.`;
+ else if (value < Emulation.DeviceModeModel.MinDeviceSize)
+ errorMessage = ls`Height must be greater than or equal to ${Emulation.DeviceModeModel.MinDeviceSize}.`;
+ else
+ valid = true;
+
+ return {valid, errorMessage};
+ }
+
+ /**
+ * @param {string} value
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
+ */
+ static scaleValidator(value) {
+ let valid = false;
+ let errorMessage;
+
+ if (!value) {
+ valid = true;
+ } else if (!/^[\d]+(\.\d+)?|\.\d+$/.test(value)) {
+ errorMessage = ls`Device pixel ratio must be a number or blank.`;
+ } else if (value > Emulation.DeviceModeModel.MaxDeviceScaleFactor) {
+ errorMessage =
+ ls`Device pixel ratio must be less than or equal to ${Emulation.DeviceModeModel.MaxDeviceScaleFactor}.`;
+ } else if (value < Emulation.DeviceModeModel.MinDeviceScaleFactor) {
+ errorMessage =
+ ls`Device pixel ratio must be greater than or equal to ${Emulation.DeviceModeModel.MinDeviceScaleFactor}.`;
+ } else {
+ valid = true;
+ }
+
+ return {valid, errorMessage};
}
/**
@@ -722,6 +766,9 @@
Emulation.DeviceModeModel.MinDeviceSize = 50;
Emulation.DeviceModeModel.MaxDeviceSize = 9999;
+Emulation.DeviceModeModel.MinDeviceScaleFactor = 0;
+Emulation.DeviceModeModel.MaxDeviceScaleFactor = 10;
+Emulation.DeviceModeModel.MaxDeviceNameLength = 50;
Emulation.DeviceModeModel._defaultMobileUserAgent =
diff --git a/front_end/emulation/DeviceModeToolbar.js b/front_end/emulation/DeviceModeToolbar.js
index d8ee0a7..00ce4a2 100644
--- a/front_end/emulation/DeviceModeToolbar.js
+++ b/front_end/emulation/DeviceModeToolbar.js
@@ -94,7 +94,7 @@
widthInput.maxLength = 4;
widthInput.title = Common.UIString('Width');
this._updateWidthInput =
- UI.bindInput(widthInput, this._applyWidth.bind(this), Emulation.DeviceModeModel.deviceSizeValidator, true);
+ UI.bindInput(widthInput, this._applyWidth.bind(this), Emulation.DeviceModeModel.widthValidator, true);
this._widthInput = widthInput;
this._widthItem = this._wrapToolbarItem(widthInput);
toolbar.appendToolbarItem(this._widthItem);
@@ -114,10 +114,12 @@
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
function validateHeight(value) {
- return !value || Emulation.DeviceModeModel.deviceSizeValidator(value);
+ if (!value)
+ return {valid: true};
+ return Emulation.DeviceModeModel.heightValidator(value);
}
}
diff --git a/front_end/emulation/DevicesSettingsTab.js b/front_end/emulation/DevicesSettingsTab.js
index c25cf53..d975994 100644
--- a/front_end/emulation/DevicesSettingsTab.js
+++ b/front_end/emulation/DevicesSettingsTab.js
@@ -200,23 +200,24 @@
const content = editor.contentElement();
const fields = content.createChild('div', 'devices-edit-fields');
- fields.createChild('div', 'hbox')
- .appendChild(editor.createInput('title', 'text', Common.UIString('Device name'), titleValidator));
+ fields.createChild('div', 'hbox').appendChild(editor.createInput('title', 'text', ls`Device Name`, titleValidator));
const screen = fields.createChild('div', 'hbox');
- screen.appendChild(editor.createInput('width', 'text', Common.UIString('Width'), sizeValidator));
- screen.appendChild(editor.createInput('height', 'text', Common.UIString('height'), sizeValidator));
- const dpr = editor.createInput('scale', 'text', Common.UIString('Device pixel ratio'), scaleValidator);
+ screen.appendChild(editor.createInput('width', 'text', ls`Width`, widthValidator));
+ screen.appendChild(editor.createInput('height', 'text', ls`Height`, heightValidator));
+ const dpr = editor.createInput('scale', 'text', ls`Device pixel ratio`, scaleValidator);
dpr.classList.add('device-edit-fixed');
screen.appendChild(dpr);
const ua = fields.createChild('div', 'hbox');
- ua.appendChild(editor.createInput('user-agent', 'text', Common.UIString('User agent string'), () => true));
- const uaType = editor.createSelect(
- 'ua-type',
- [
- Emulation.DeviceModeModel.UA.Mobile, Emulation.DeviceModeModel.UA.MobileNoTouch,
- Emulation.DeviceModeModel.UA.Desktop, Emulation.DeviceModeModel.UA.DesktopTouch
- ],
- () => true);
+ ua.appendChild(editor.createInput('user-agent', 'text', ls`User agent string`, () => {
+ return {valid: true};
+ }));
+ const uaTypeOptions = [
+ Emulation.DeviceModeModel.UA.Mobile, Emulation.DeviceModeModel.UA.MobileNoTouch,
+ Emulation.DeviceModeModel.UA.Desktop, Emulation.DeviceModeModel.UA.DesktopTouch
+ ];
+ const uaType = editor.createSelect('ua-type', uaTypeOptions, () => {
+ return {valid: true};
+ }, ls`User agent type`);
uaType.classList.add('device-edit-fixed');
ua.appendChild(uaType);
@@ -226,31 +227,51 @@
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function titleValidator(item, index, input) {
+ let valid = false;
+ let errorMessage;
+
const value = input.value.trim();
- return value.length > 0 && value.length < 50;
+ if (value.length >= Emulation.DeviceModeModel.MaxDeviceNameLength)
+ errorMessage = ls`Device name must be less than ${Emulation.DeviceModeModel.MaxDeviceNameLength} characters.`;
+ else if (value.length === 0)
+ errorMessage = ls`Device name cannot be empty.`;
+ else
+ valid = true;
+
+ return {valid, errorMessage};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
- function sizeValidator(item, index, input) {
- return Emulation.DeviceModeModel.deviceSizeValidator(input.value);
+ function widthValidator(item, index, input) {
+ return Emulation.DeviceModeModel.widthValidator(input.value);
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
+ */
+ function heightValidator(item, index, input) {
+ return Emulation.DeviceModeModel.heightValidator(input.value);
+ }
+
+ /**
+ * @param {*} item
+ * @param {number} index
+ * @param {!HTMLInputElement|!HTMLSelectElement} input
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function scaleValidator(item, index, input) {
- return Emulation.DeviceModeModel.deviceScaleFactorValidator(input.value);
+ return Emulation.DeviceModeModel.scaleValidator(input.value);
}
}
-};
+};
\ No newline at end of file
diff --git a/front_end/emulation/GeolocationsSettingsTab.js b/front_end/emulation/GeolocationsSettingsTab.js
index 5cae4d2..9229d72 100644
--- a/front_end/emulation/GeolocationsSettingsTab.js
+++ b/front_end/emulation/GeolocationsSettingsTab.js
@@ -153,33 +153,36 @@
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function titleValidator(item, index, input) {
const value = input.value.trim();
- return value.length > 0 && value.length < 50;
+ const valid = value.length > 0 && value.length < 50;
+ return {valid};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function latValidator(item, index, input) {
const value = input.value.trim();
- return !value || (/^-?[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= -90 && value <= 90);
+ const valid = !value || (/^-?[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= -90 && value <= 90);
+ return {valid};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function longValidator(item, index, input) {
const value = input.value.trim();
- return !value || (/^-?[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= -180 && value <= 180);
+ const valid = !value || (/^-?[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= -180 && value <= 180);
+ return {valid};
}
}
};
diff --git a/front_end/mobile_throttling/ThrottlingSettingsTab.js b/front_end/mobile_throttling/ThrottlingSettingsTab.js
index e303163..4c61600 100644
--- a/front_end/mobile_throttling/ThrottlingSettingsTab.js
+++ b/front_end/mobile_throttling/ThrottlingSettingsTab.js
@@ -168,33 +168,36 @@
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function titleValidator(item, index, input) {
const value = input.value.trim();
- return value.length > 0 && value.length < 50;
+ const valid = value.length > 0 && value.length < 50;
+ return {valid};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function throughputValidator(item, index, input) {
const value = input.value.trim();
- return !value || (/^[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= 0 && value <= 10000000);
+ const valid = !value || (/^[\d]+(\.\d+)?|\.\d+$/.test(value) && value >= 0 && value <= 10000000);
+ return {valid};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function latencyValidator(item, index, input) {
const value = input.value.trim();
- return !value || (/^[\d]+$/.test(value) && value >= 0 && value <= 1000000);
+ const valid = !value || (/^[\d]+$/.test(value) && value >= 0 && value <= 1000000);
+ return {valid};
}
}
};
diff --git a/front_end/network/BlockedURLsPane.js b/front_end/network/BlockedURLsPane.js
index 3991d91..2ec93c9 100644
--- a/front_end/network/BlockedURLsPane.js
+++ b/front_end/network/BlockedURLsPane.js
@@ -154,10 +154,11 @@
titles.createChild('div').textContent =
Common.UIString('Text pattern to block matching requests; use * for wildcard');
const fields = content.createChild('div', 'blocked-url-edit-row');
- const urlInput = editor.createInput(
- 'url', 'text', '',
- (item, index, input) =>
- !!input.value && !this._manager.blockedPatterns().find(pattern => pattern.url === input.value));
+ const validator = (item, index, input) => {
+ const valid = !!input.value && !this._manager.blockedPatterns().find(pattern => pattern.url === input.value);
+ return {valid};
+ };
+ const urlInput = editor.createInput('url', 'text', '', validator);
fields.createChild('div', 'blocked-url-edit-value').appendChild(urlInput);
return editor;
}
diff --git a/front_end/network/NetworkManageCustomHeadersView.js b/front_end/network/NetworkManageCustomHeadersView.js
index 4136dd9..811684f 100644
--- a/front_end/network/NetworkManageCustomHeadersView.js
+++ b/front_end/network/NetworkManageCustomHeadersView.js
@@ -142,13 +142,14 @@
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @this {Network.NetworkManageCustomHeadersView}
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function validateHeader(item, index, input) {
+ let valid = true;
const headerId = editor.control('header').value.trim().toLowerCase();
if (this._columnConfigs.has(headerId) && item.header !== headerId)
- return false;
- return true;
+ valid = false;
+ return {valid};
}
}
};
diff --git a/front_end/node_main/NodeConnectionsPanel.js b/front_end/node_main/NodeConnectionsPanel.js
index ff9ed37..7c49dbb 100644
--- a/front_end/node_main/NodeConnectionsPanel.js
+++ b/front_end/node_main/NodeConnectionsPanel.js
@@ -170,14 +170,14 @@
* @param {!Adb.PortForwardingRule} rule
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function addressValidator(rule, index, input) {
const match = input.value.trim().match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
if (!match)
- return false;
+ return {valid: false};
const port = parseInt(match[2], 10);
- return port <= 65535;
+ return {valid: port <= 65535};
}
}
};
diff --git a/front_end/persistence/EditFileSystemView.js b/front_end/persistence/EditFileSystemView.js
index 420468b..2fe58d0 100644
--- a/front_end/persistence/EditFileSystemView.js
+++ b/front_end/persistence/EditFileSystemView.js
@@ -164,7 +164,7 @@
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
* @this {Persistence.EditFileSystemView}
*/
function pathPrefixValidator(item, index, input) {
@@ -173,9 +173,9 @@
Persistence.isolatedFileSystemManager.fileSystem(this._fileSystemPath).excludedFolders().size;
for (let i = 0; i < configurableCount; ++i) {
if (i !== index && this._excludedFolders[i] === prefix)
- return false;
+ return {valid: false};
}
- return !!prefix;
+ return {valid: !!prefix};
}
}
diff --git a/front_end/persistence/WorkspaceSettingsTab.js b/front_end/persistence/WorkspaceSettingsTab.js
index 36a865b..cbff5f6 100644
--- a/front_end/persistence/WorkspaceSettingsTab.js
+++ b/front_end/persistence/WorkspaceSettingsTab.js
@@ -63,7 +63,7 @@
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
function regexValidator(value) {
let regex;
@@ -71,7 +71,8 @@
regex = new RegExp(value);
} catch (e) {
}
- return !!regex;
+ const valid = !!regex;
+ return {valid};
}
}
diff --git a/front_end/sdk/EmulationModel.js b/front_end/sdk/EmulationModel.js
index 6e6fcc2..9e788d1 100644
--- a/front_end/sdk/EmulationModel.js
+++ b/front_end/sdk/EmulationModel.js
@@ -192,8 +192,8 @@
if (!latitudeString && !longitudeString)
return null;
- const isLatitudeValid = SDK.EmulationModel.Geolocation.latitudeValidator(latitudeString);
- const isLongitudeValid = SDK.EmulationModel.Geolocation.longitudeValidator(longitudeString);
+ const {valid: isLatitudeValid} = SDK.EmulationModel.Geolocation.latitudeValidator(latitudeString);
+ const {valid: isLongitudeValid} = SDK.EmulationModel.Geolocation.longitudeValidator(longitudeString);
if (!isLatitudeValid && !isLongitudeValid)
return null;
@@ -205,20 +205,22 @@
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
static latitudeValidator(value) {
const numValue = parseFloat(value);
- return /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value) && numValue >= -90 && numValue <= 90;
+ const valid = /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value) && numValue >= -90 && numValue <= 90;
+ return {valid};
}
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
static longitudeValidator(value) {
const numValue = parseFloat(value);
- return /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value) && numValue >= -180 && numValue <= 180;
+ const valid = /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value) && numValue >= -180 && numValue <= 180;
+ return {valid};
}
/**
@@ -261,9 +263,9 @@
if (!alphaString && !betaString && !gammaString)
return null;
- const isAlphaValid = SDK.EmulationModel.DeviceOrientation.validator(alphaString);
- const isBetaValid = SDK.EmulationModel.DeviceOrientation.validator(betaString);
- const isGammaValid = SDK.EmulationModel.DeviceOrientation.validator(gammaString);
+ const {valid: isAlphaValid} = SDK.EmulationModel.DeviceOrientation.validator(alphaString);
+ const {valid: isBetaValid} = SDK.EmulationModel.DeviceOrientation.validator(betaString);
+ const {valid: isGammaValid} = SDK.EmulationModel.DeviceOrientation.validator(gammaString);
if (!isAlphaValid && !isBetaValid && !isGammaValid)
return null;
@@ -277,10 +279,11 @@
/**
* @param {string} value
- * @return {boolean}
+ * @return {{valid: boolean, errorMessage: (string|undefined)}}
*/
static validator(value) {
- return /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value);
+ const valid = /^([+-]?[\d]+(\.\d+)?|[+-]?\.\d+)$/.test(value);
+ return {valid};
}
/**
diff --git a/front_end/settings/FrameworkBlackboxSettingsTab.js b/front_end/settings/FrameworkBlackboxSettingsTab.js
index c657105..430c8ea 100644
--- a/front_end/settings/FrameworkBlackboxSettingsTab.js
+++ b/front_end/settings/FrameworkBlackboxSettingsTab.js
@@ -153,14 +153,14 @@
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
* @this {Settings.FrameworkBlackboxSettingsTab}
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function patternValidator(item, index, input) {
const pattern = input.value.trim();
const patterns = this._setting.getAsArray();
for (let i = 0; i < patterns.length; ++i) {
if (i !== index && patterns[i].pattern === pattern)
- return false;
+ return {valid: false};
}
let regex;
@@ -168,17 +168,17 @@
regex = new RegExp(pattern);
} catch (e) {
}
- return !!(pattern && regex);
+ return {valid: !!(pattern && regex)};
}
/**
* @param {*} item
* @param {number} index
* @param {!HTMLInputElement|!HTMLSelectElement} input
- * @return {boolean}
+ * @return {!UI.ListWidget.ValidatorResult}
*/
function behaviorValidator(item, index, input) {
- return true;
+ return {valid: true};
}
}
};
diff --git a/front_end/ui/ARIAUtils.js b/front_end/ui/ARIAUtils.js
index 21b70d0..0de8216 100644
--- a/front_end/ui/ARIAUtils.js
+++ b/front_end/ui/ARIAUtils.js
@@ -18,6 +18,14 @@
/**
* @param {!Element} element
*/
+UI.ARIAUtils.markAsAlert = function(element) {
+ element.setAttribute('role', 'alert');
+ element.setAttribute('aria-live', 'polite');
+};
+
+/**
+ * @param {!Element} element
+ */
UI.ARIAUtils.markAsButton = function(element) {
element.setAttribute('role', 'button');
};
@@ -235,6 +243,17 @@
* @param {!Element} element
* @param {boolean} value
*/
+UI.ARIAUtils.setInvalid = function(element, value) {
+ if (value)
+ element.setAttribute('aria-invalid', value);
+ else
+ element.removeAttribute('aria-invalid');
+};
+
+/**
+ * @param {!Element} element
+ * @param {boolean} value
+ */
UI.ARIAUtils.setPressed = function(element, value) {
// aria-pressed behaves differently for false and undefined.
// Often times undefined values are unintentionally typed as booleans.
diff --git a/front_end/ui/ListWidget.js b/front_end/ui/ListWidget.js
index 0ba4777..c6ee392 100644
--- a/front_end/ui/ListWidget.js
+++ b/front_end/ui/ListWidget.js
@@ -272,6 +272,9 @@
this._contentElement = this.element.createChild('div', 'editor-content');
+ this._errorMessageContainer = this.element.createChild('div', 'list-widget-input-validation-error');
+ UI.ARIAUtils.markAsAlert(this._errorMessageContainer);
+
const buttonsRow = this.element.createChild('div', 'editor-buttons');
this._commitButton = UI.createTextButton('', this._commitClicked.bind(this), '', true /* primary */);
buttonsRow.appendChild(this._commitButton);
@@ -296,7 +299,7 @@
this._controls = [];
/** @type {!Map<string, !HTMLInputElement|!HTMLSelectElement>} */
this._controlByName = new Map();
- /** @type {!Array<function(!T, number, (!HTMLInputElement|!HTMLSelectElement)):boolean>} */
+ /** @type {!Array<function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult>} */
this._validators = [];
/** @type {?function()} */
@@ -320,7 +323,7 @@
* @param {string} name
* @param {string} type
* @param {string} title
- * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)):boolean} validator
+ * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult} validator
* @return {!HTMLInputElement}
*/
createInput(name, type, title, validator) {
@@ -328,6 +331,7 @@
input.placeholder = title;
input.addEventListener('input', this._validateControls.bind(this, false), false);
input.addEventListener('blur', this._validateControls.bind(this, false), false);
+ UI.ARIAUtils.setAccessibleName(input, title);
this._controlByName.set(name, input);
this._controls.push(input);
this._validators.push(validator);
@@ -337,16 +341,21 @@
/**
* @param {string} name
* @param {!Array<string>} options
- * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)):boolean} validator
+ * @param {function(!T, number, (!HTMLInputElement|!HTMLSelectElement)): !UI.ListWidget.ValidatorResult} validator
+ * @param {string=} title
* @return {!HTMLSelectElement}
*/
- createSelect(name, options, validator) {
+ createSelect(name, options, validator, title) {
const select = /** @type {!HTMLSelectElement} */ (createElementWithClass('select', 'chrome-select'));
for (let index = 0; index < options.length; ++index) {
const option = select.createChild('option');
option.value = options[index];
option.textContent = options[index];
}
+ if (title) {
+ select.title = title;
+ UI.ARIAUtils.setAccessibleName(select, title);
+ }
select.addEventListener('input', this._validateControls.bind(this, false), false);
select.addEventListener('blur', this._validateControls.bind(this, false), false);
this._controlByName.set(name, select);
@@ -368,10 +377,20 @@
*/
_validateControls(forceValid) {
let allValid = true;
+ this._errorMessageContainer.textContent = '';
for (let index = 0; index < this._controls.length; ++index) {
const input = this._controls[index];
- const valid = this._validators[index].call(null, this._item, this._index, input);
+ const {valid, errorMessage} = this._validators[index].call(null, this._item, this._index, input);
+
input.classList.toggle('error-input', !valid && !forceValid);
+ if (valid || forceValid)
+ UI.ARIAUtils.setInvalid(input, false);
+ else
+ UI.ARIAUtils.setInvalid(input, true);
+
+ if (!forceValid && errorMessage && !this._errorMessageContainer.textContent)
+ this._errorMessageContainer.textContent = errorMessage;
+
allValid &= valid;
}
this._commitButton.disabled = !allValid;
@@ -418,3 +437,6 @@
cancel();
}
};
+
+/** @typedef {{valid: boolean, errorMessage: (string|undefined)}} */
+UI.ListWidget.ValidatorResult;
diff --git a/front_end/ui/UIUtils.js b/front_end/ui/UIUtils.js
index a9e56d4..ba90907 100644
--- a/front_end/ui/UIUtils.js
+++ b/front_end/ui/UIUtils.js
@@ -1503,7 +1503,7 @@
/**
* @param {!Element} input
* @param {function(string)} apply
- * @param {function(string):boolean} validate
+ * @param {function(string):{valid: boolean, errorMessage: (string|undefined)}} validate
* @param {boolean} numeric
* @param {number=} modifierMultiplier
* @return {function(string)}
@@ -1519,7 +1519,7 @@
}
function onChange() {
- const valid = validate(input.value);
+ const {valid} = validate(input.value);
input.classList.toggle('error-input', !valid);
if (valid)
apply(input.value);
@@ -1530,7 +1530,8 @@
*/
function onKeyDown(event) {
if (isEnterKey(event)) {
- if (validate(input.value))
+ const {valid} = validate(input.value);
+ if (valid)
apply(input.value);
event.preventDefault();
return;
@@ -1541,7 +1542,8 @@
const value = UI._modifiedFloatNumber(parseFloat(input.value), event, modifierMultiplier);
const stringValue = value ? String(value) : '';
- if (!validate(stringValue) || !value)
+ const {valid} = validate(stringValue);
+ if (!valid || !value)
return;
input.value = stringValue;
@@ -1555,7 +1557,7 @@
function setValue(value) {
if (value === input.value)
return;
- const valid = validate(value);
+ const {valid} = validate(value);
input.classList.toggle('error-input', !valid);
input.value = value;
}
diff --git a/front_end/ui/listWidget.css b/front_end/ui/listWidget.css
index b1680a3..deef7a0 100644
--- a/front_end/ui/listWidget.css
+++ b/front_end/ui/listWidget.css
@@ -29,6 +29,11 @@
background: hsl(0, 0%, 96%);
}
+.list-widget-input-validation-error {
+ color: #db1600;
+ margin: 0 5px;
+}
+
.controls-container {
display: flex;
flex-direction: row;