Commit f6dec5b7 authored by My Nguyen's avatar My Nguyen Committed by Commit Bot

[OsSettingsLanguages] Add Spell check languages list for input_page

Follow mock at http://go/cros-lang-settings-ux-slide#slide=23

Will add other components in following CLs.
Current view enabled by policy: http://screen/37LZsEYnjeA5qLk
When spell check is disabled: http://screen/7xgUwHJz82HMMpK
View when no spell check language: http://screen/Aedx2EsgeFKUFys

Note: All strings are not finalised so they are translateable false
and no screenshots required.

Bug: 1113439
Change-Id: I1ce9110f3f1d5a295ccc815ae346de3460a340f8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2360093
Commit-Queue: My Nguyen <myy@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarRegan Hsu <hsuregan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#805610}
parent 50a89210
...@@ -301,6 +301,12 @@ ...@@ -301,6 +301,12 @@
<message name="IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_DISABLED_REASON" translateable="false" desc="Text that indicates to the user that spell check options are disabled because none of the languages they have added have spell check support."> <message name="IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_DISABLED_REASON" translateable="false" desc="Text that indicates to the user that spell check options are disabled because none of the languages they have added have spell check support.">
Spell check isn’t supported for the languages you selected Spell check isn’t supported for the languages you selected
</message> </message>
<message name="IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_LANGUAGES_LIST_TITLE" translateable="false" desc="Title for the list of languages that support spell check, from which users can enable or disable spell check for.">
Spell check languages
</message>
<message name="IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_LANGUAGES_LIST_DESCRIPTION" translateable="false" desc="Description for the list of languages that support spell check, from which users can enable or disable spell check for.">
Languages available for spell check is based on your languages settings
</message>
<message name="IDS_OS_SETTINGS_LANGUAGES_LIST_TITLE" desc="Title for the list of the user's preferred written languages."> <message name="IDS_OS_SETTINGS_LANGUAGES_LIST_TITLE" desc="Title for the list of the user's preferred written languages.">
Languages Languages
</message> </message>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<link rel="import" href="chrome://resources/html/assert.html"> <link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/html/cr/ui/focus_without_ink.html"> <link rel="import" href="chrome://resources/html/cr/ui/focus_without_ink.html">
<link rel="import" href="chrome://resources/html/i18n_behavior.html"> <link rel="import" href="chrome://resources/html/i18n_behavior.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html"> <link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
<link rel="import" href="add_input_methods_dialog.html"> <link rel="import" href="add_input_methods_dialog.html">
<link rel="import" href="input_method_util.html"> <link rel="import" href="input_method_util.html">
...@@ -20,11 +21,15 @@ ...@@ -20,11 +21,15 @@
<dom-module id="os-settings-input-page"> <dom-module id="os-settings-input-page">
<template> <template>
<style include="settings-shared"> <style include="settings-shared iron-flex">
h2 { h2 {
padding-inline-start: var(--cr-section-padding); padding-inline-start: var(--cr-section-padding);
} }
cr-policy-pref-indicator {
margin-inline-end: var(--settings-controlled-by-spacing);
}
.bottom-margin { .bottom-margin {
margin-bottom: var(--cr-section-vertical-margin); margin-bottom: var(--cr-section-vertical-margin);
} }
...@@ -33,6 +38,10 @@ ...@@ -33,6 +38,10 @@
margin-inline-end: 0; margin-inline-end: 0;
} }
.name-with-error[disabled] {
pointer-events: none;
}
/* The default implementation of the actionable list item makes the /* The default implementation of the actionable list item makes the
* entire list item row a button such that clicking anywhere will * entire list item row a button such that clicking anywhere will
* activate the action of the list item. The input method list behaves * activate the action of the list item. The input method list behaves
...@@ -46,6 +55,15 @@ ...@@ -46,6 +55,15 @@
cursor: auto; cursor: auto;
} }
.subsection {
padding-inline-end: var(--cr-section-padding);
padding-inline-start: var(--cr-section-indent-padding);
}
.subsection .list-frame {
padding-inline-end: 0;
}
.internal-wrapper, .internal-wrapper,
.external-wrapper { .external-wrapper {
display: flex; display: flex;
...@@ -122,12 +140,57 @@ ...@@ -122,12 +140,57 @@
</div> </div>
<settings-toggle-button id="enableSpellcheckingToggle" class="hr" <settings-toggle-button id="enableSpellcheckingToggle" class="hr"
label="$i18n{spellCheckTitle}" label="$i18n{spellCheckTitle}"
sub-label="[[getSpellCheckSubLabel_(spellCheckLanguages_)]]"
pref="{{prefs.browser.enable_spellchecking}}" pref="{{prefs.browser.enable_spellchecking}}"
disabled="[[!spellCheckLanguages_.length]]" disabled="[[!spellCheckLanguages_.length]]"
on-settings-boolean-control-change="onSpellcheckToggleChange_" on-settings-boolean-control-change="onSpellcheckToggleChange_"
deep-link-focus-id$="[[Setting.kSpellCheck]]"> deep-link-focus-id$="[[Setting.kSpellCheck]]">
</settings-toggle-button> </settings-toggle-button>
<div class="subsection">
<div id="spellCheckLanguagesList">
<div class="single-column">
<div aria-describedby="spellChecklanguagesListDescription">
$i18n{spellCheckLanguagesListTitle}
</div>
<div class="secondary" id="spellChecklanguagesListDescription"
aria-hidden="true">
$i18n{spellCheckLanguagesListDescription}
</div>
</div>
<div class="list-frame vertical-list" role="list">
<template is="dom-repeat" items="[[spellCheckLanguages_]]"
mutable-data>
<div class="list-item">
<div class="flex name-with-error" aria-hidden="true"
on-click="onSpellCheckNameClick_" actionable
disabled$="[[isSpellCheckNameClickDisabled_(item, item.*,
prefs.browser.enable_spellchecking.*)]]">
[[item.language.displayName]]
</div>
<template is="dom-if" if="[[!
item.downloadDictionaryFailureCount]]">
<template is="dom-if" if="[[!item.isManaged]]">
<cr-toggle on-change="onSpellCheckLanguageChange_"
disabled="[[!prefs.browser.enable_spellchecking.value]]"
checked="[[item.spellCheckEnabled]]"
aria-label="[[item.language.displayName]]">
</cr-toggle>
</template>
<template is="dom-if" if="[[item.isManaged]]">
<cr-policy-pref-indicator
pref="[[getIndicatorPrefForManagedSpellcheckLanguage_(
item.spellCheckEnabled)]]">
</cr-policy-pref-indicator>
<cr-toggle disabled="true" class="managed-toggle"
checked="[[item.spellCheckEnabled]]"
aria-label="[[item.language.displayName]]">
</cr-toggle>
</template>
</template>
</div>
</template>
</div>
</div>
</div>
</div> </div>
<template is="dom-if" if="[[showAddInputMethodsDialog_]]" restamp> <template is="dom-if" if="[[showAddInputMethodsDialog_]]" restamp>
......
...@@ -17,9 +17,7 @@ Polymer({ ...@@ -17,9 +17,7 @@ Polymer({
], ],
properties: { properties: {
/** /* Preferences state. */
* Preferences state.
*/
prefs: { prefs: {
type: Object, type: Object,
notify: true, notify: true,
...@@ -37,14 +35,11 @@ Polymer({ ...@@ -37,14 +35,11 @@ Polymer({
/** @type {!LanguageHelper} */ /** @type {!LanguageHelper} */
languageHelper: Object, languageHelper: Object,
/** /** @private {!Array<!LanguageState|!ForcedLanguageState>|undefined} */
* @private {!Array<!LanguageState|!ForcedLanguageState>}
*/
spellCheckLanguages_: { spellCheckLanguages_: {
type: Array, type: Array,
value() { computed:
return []; 'getSpellCheckLanguages_(languages.enabled.*, languages.forcedSpellCheckLanguages.*)',
},
}, },
/** @private */ /** @private */
...@@ -85,6 +80,10 @@ Polymer({ ...@@ -85,6 +80,10 @@ Polymer({
settings.LanguagesMetricsProxyImpl.getInstance(); settings.LanguagesMetricsProxyImpl.getInstance();
}, },
observers: [
'updateSpellcheckPref_(spellCheckLanguages_)',
],
/** /**
* @param {!settings.Route} route * @param {!settings.Route} route
* @param {!settings.Route} oldRoute * @param {!settings.Route} oldRoute
...@@ -249,19 +248,100 @@ Polymer({ ...@@ -249,19 +248,100 @@ Polymer({
/** /**
* @return {string|undefined} * @return {string|undefined}
* @param {!Event} e
* @private * @private
*/ */
getSpellCheckSubLabel_() { onSpellcheckToggleChange_(e) {
return this.spellCheckLanguages_.length ? this.languagesMetricsProxy_.recordToggleSpellCheck(e.target.checked);
undefined : // equivalent to not setting the sublabel in the HTML.
this.i18n('spellCheckDisabledReason');
}, },
/** /**
* @param {!Event} e * Returns the value to use as the |pref| attribute for the policy indicator
* of spellcheck languages, based on whether or not the language is enabled.
* @param {boolean} isEnabled Whether the language is enabled or not.
* @private * @private
*/ */
onSpellcheckToggleChange_(e) { getIndicatorPrefForManagedSpellcheckLanguage_(isEnabled) {
this.languagesMetricsProxy_.recordToggleSpellCheck(e.target.checked); return isEnabled ? this.getPref('spellcheck.forced_dictionaries') :
this.getPref('spellcheck.blacklisted_dictionaries');
},
/**
* Returns an array of enabled languages that support spell check, plus
* spellcheck languages that are force-enabled by policy.
* @return {!Array<!LanguageState|!ForcedLanguageState>|undefined}
* @private
*/
getSpellCheckLanguages_() {
if (this.languages === undefined) {
return undefined;
}
const combinedLanguages =
this.languages.enabled.concat(this.languages.forcedSpellCheckLanguages);
const supportedSpellcheckLanguagesSet = new Set();
const supportedSpellcheckLanguages = [];
combinedLanguages.forEach(languageState => {
if (!supportedSpellcheckLanguagesSet.has(languageState.language.code) &&
languageState.language.supportsSpellcheck) {
supportedSpellcheckLanguages.push(languageState);
supportedSpellcheckLanguagesSet.add(languageState.language.code);
}
});
return supportedSpellcheckLanguages;
},
/** @private */
updateSpellcheckPref_() {
if (this.spellCheckLanguages_ === undefined) {
return;
}
// TODO(crbug/1126239): Investigate feasibility of moving this pref update
// to spellcheck_service.
if (this.spellCheckLanguages_.length === 0) {
// If there are no supported spell check languages, automatically turn
// off spell check to indicate no spell check will happen.
this.setPrefValue('browser.enable_spellchecking', false);
}
},
/**
* Handler for enabling or disabling spell check for a specific language.
* @param {!{target: Element, model: !{item: !LanguageState}}} e
* @private
*/
onSpellCheckLanguageChange_(e) {
const item = e.model.item;
if (!item.language.supportsSpellcheck) {
return;
}
this.languageHelper.toggleSpellCheck(
item.language.code, !item.spellCheckEnabled);
},
/**
* Handler for clicking on the name of the language. The action taken must
* match the control that is available.
* @param {!{target: Element, model: !{item: !LanguageState}}} e
* @private
*/
onSpellCheckNameClick_(e) {
assert(!this.isSpellCheckNameClickDisabled_(e.model.item));
this.onSpellCheckLanguageChange_(e);
},
/**
* Name only supports clicking when language is not managed, supports
* spellcheck, and the dictionary has been downloaded with no errors.
* @param {!LanguageState|!ForcedLanguageState} item
* @return {boolean}
* @private
*/
isSpellCheckNameClickDisabled_(item) {
return item.isManaged || item.downloadDictionaryFailureCount > 0 ||
!this.getPref('browser.enable_spellchecking').value;
}, },
}); });
...@@ -291,6 +291,10 @@ void AddInputPageStringsV2(content::WebUIDataSource* html_source) { ...@@ -291,6 +291,10 @@ void AddInputPageStringsV2(content::WebUIDataSource* html_source) {
{"spellCheckTitle", IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_TITLE}, {"spellCheckTitle", IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_TITLE},
{"spellCheckDisabledReason", {"spellCheckDisabledReason",
IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_DISABLED_REASON}, IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_DISABLED_REASON},
{"spellCheckLanguagesListTitle",
IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_LANGUAGES_LIST_TITLE},
{"spellCheckLanguagesListDescription",
IDS_OS_SETTINGS_LANGUAGES_SPELL_CHECK_LANGUAGES_LIST_DESCRIPTION},
}; };
AddLocalizedStringsBulk(html_source, kLocalizedStrings); AddLocalizedStringsBulk(html_source, kLocalizedStrings);
} }
......
...@@ -21,6 +21,10 @@ suite('input page', () => { ...@@ -21,6 +21,10 @@ suite('input page', () => {
let inputPage; let inputPage;
/** @type {!settings.LanguagesMetricsProxy} */ /** @type {!settings.LanguagesMetricsProxy} */
let metricsProxy; let metricsProxy;
/** @type {!settings.LanguagesBrowserProxy} */
let browserProxy;
/** @type {!LanguagesHelper} */
let languageHelper;
suiteSetup(() => { suiteSetup(() => {
CrSettingsPrefs.deferInitialization = true; CrSettingsPrefs.deferInitialization = true;
...@@ -37,7 +41,7 @@ suite('input page', () => { ...@@ -37,7 +41,7 @@ suite('input page', () => {
return CrSettingsPrefs.initialized.then(() => { return CrSettingsPrefs.initialized.then(() => {
// Set up test browser proxy. // Set up test browser proxy.
const browserProxy = new settings.TestLanguagesBrowserProxy(); browserProxy = new settings.TestLanguagesBrowserProxy();
settings.LanguagesBrowserProxyImpl.instance_ = browserProxy; settings.LanguagesBrowserProxyImpl.instance_ = browserProxy;
// Sets up test metrics proxy. // Sets up test metrics proxy.
...@@ -62,6 +66,7 @@ suite('input page', () => { ...@@ -62,6 +66,7 @@ suite('input page', () => {
test_util.fakeDataBind(settingsLanguages, inputPage, 'languages'); test_util.fakeDataBind(settingsLanguages, inputPage, 'languages');
inputPage.languageHelper = settingsLanguages.languageHelper; inputPage.languageHelper = settingsLanguages.languageHelper;
test_util.fakeDataBind(settingsLanguages, inputPage, 'language-helper'); test_util.fakeDataBind(settingsLanguages, inputPage, 'language-helper');
languageHelper = inputPage.languageHelper;
document.body.appendChild(inputPage); document.body.appendChild(inputPage);
}); });
}); });
...@@ -196,4 +201,205 @@ suite('input page', () => { ...@@ -196,4 +201,205 @@ suite('input page', () => {
await metricsProxy.whenCalled('recordAddInputMethod'); await metricsProxy.whenCalled('recordAddInputMethod');
}); });
}); });
suite('spell check', () => {
let spellCheckToggle;
let spellCheckListContainer;
let spellCheckList;
setup(() => {
// spell check is initially on
spellCheckToggle = inputPage.$.enableSpellcheckingToggle;
assertTrue(!!spellCheckToggle);
assertTrue(spellCheckToggle.checked);
spellCheckListContainer = inputPage.$$('#spellCheckLanguagesList');
assertTrue(!!spellCheckListContainer);
// two languages are in the list, with en-US on and sw off.
spellCheckList = spellCheckListContainer.querySelectorAll('.list-item');
assertEquals(2, spellCheckList.length);
assertTrue(
spellCheckList[0].textContent.includes('English (United States)'));
assertTrue(spellCheckList[0].querySelector('cr-toggle').checked);
assertTrue(spellCheckList[1].textContent.includes('Swahili'));
assertFalse(spellCheckList[1].querySelector('cr-toggle').checked);
});
test('toggles a spell check language add/remove it from dictionary', () => {
assertDeepEquals(
['en-US'], languageHelper.prefs.spellcheck.dictionaries.value);
// Get toggle for en-US.
const spellCheckLanguageToggle =
spellCheckList[0].querySelector('cr-toggle');
// toggle off
spellCheckLanguageToggle.click();
assertFalse(spellCheckLanguageToggle.checked);
assertDeepEquals([], languageHelper.prefs.spellcheck.dictionaries.value);
// toggle on
spellCheckLanguageToggle.click();
assertTrue(spellCheckLanguageToggle.checked);
assertDeepEquals(
['en-US'], languageHelper.prefs.spellcheck.dictionaries.value);
});
test(
'clicks a spell check language name add/remove it from dictionary',
() => {
assertDeepEquals(
['en-US'], languageHelper.prefs.spellcheck.dictionaries.value);
// Get toggle for en-US.
const spellCheckLanguageToggle =
spellCheckList[0].querySelector('cr-toggle');
// toggle off by clicking name
spellCheckList[0].querySelector('.name-with-error').click();
Polymer.dom.flush();
assertFalse(spellCheckLanguageToggle.checked);
assertDeepEquals(
[], languageHelper.prefs.spellcheck.dictionaries.value);
// toggle on by clicking name
spellCheckList[0].querySelector('.name-with-error').click();
Polymer.dom.flush();
assertTrue(spellCheckLanguageToggle.checked);
assertDeepEquals(
['en-US'], languageHelper.prefs.spellcheck.dictionaries.value);
});
test('shows force-on existing spell check language', () => {
// Force-enable an existing language via policy.
languageHelper.setPrefValue('spellcheck.forced_dictionaries', ['sw']);
Polymer.dom.flush();
const newSpellCheckList =
spellCheckListContainer.querySelectorAll('.list-item');
assertEquals(2, newSpellCheckList.length);
const forceEnabledSwLanguageRow = newSpellCheckList[1];
assertTrue(!!forceEnabledSwLanguageRow);
assertTrue(!!forceEnabledSwLanguageRow.querySelector(
'cr-policy-pref-indicator'));
assertTrue(
forceEnabledSwLanguageRow.querySelector('.managed-toggle').checked);
assertTrue(
forceEnabledSwLanguageRow.querySelector('.managed-toggle').disabled);
assertEquals(
getComputedStyle(
forceEnabledSwLanguageRow.querySelector('.name-with-error'))
.pointerEvents,
'none');
});
test('shows force-on non-enabled spell check language', () => {
// Force-enable a new language via policy.
languageHelper.setPrefValue('spellcheck.forced_dictionaries', ['nb']);
Polymer.dom.flush();
const newSpellCheckList =
spellCheckListContainer.querySelectorAll('.list-item');
assertEquals(3, newSpellCheckList.length);
const forceEnabledNbLanguageRow = newSpellCheckList[2];
assertTrue(!!forceEnabledNbLanguageRow);
assertTrue(!!forceEnabledNbLanguageRow.querySelector(
'cr-policy-pref-indicator'));
assertTrue(
forceEnabledNbLanguageRow.querySelector('.managed-toggle').checked);
assertTrue(
forceEnabledNbLanguageRow.querySelector('.managed-toggle').disabled);
assertEquals(
getComputedStyle(
forceEnabledNbLanguageRow.querySelector('.name-with-error'))
.pointerEvents,
'none');
});
test(
'does not show force-off spell check when language is not enabled',
() => {
// Force-disable a language via policy.
languageHelper.setPrefValue(
'spellcheck.blacklisted_dictionaries', ['nb']);
Polymer.dom.flush();
const newSpellCheckList =
spellCheckListContainer.querySelectorAll('.list-item');
assertEquals(2, newSpellCheckList.length);
});
test('shows force-off spell check when language is enabled', () => {
// Force-disable a language via policy.
languageHelper.setPrefValue(
'spellcheck.blacklisted_dictionaries', ['nb']);
languageHelper.enableLanguage('nb');
Polymer.dom.flush();
const newSpellCheckList =
spellCheckListContainer.querySelectorAll('.list-item');
assertEquals(3, newSpellCheckList.length);
const forceDisabledNbLanguageRow = newSpellCheckList[2];
assertTrue(!!forceDisabledNbLanguageRow.querySelector(
'cr-policy-pref-indicator'));
assertFalse(
forceDisabledNbLanguageRow.querySelector('.managed-toggle').checked);
assertTrue(
forceDisabledNbLanguageRow.querySelector('.managed-toggle').disabled);
assertEquals(
getComputedStyle(
forceDisabledNbLanguageRow.querySelector('.name-with-error'))
.pointerEvents,
'none');
});
test('toggle off disables toggle and click event', () => {
// Initially, both toggles are enabled
assertFalse(spellCheckList[0].querySelector('cr-toggle').disabled);
assertFalse(spellCheckList[1].querySelector('cr-toggle').disabled);
assertEquals(
getComputedStyle(spellCheckList[0].querySelector('.name-with-error'))
.pointerEvents,
'auto');
assertEquals(
getComputedStyle(spellCheckList[1].querySelector('.name-with-error'))
.pointerEvents,
'auto');
spellCheckToggle.click();
assertFalse(spellCheckToggle.checked);
assertTrue(spellCheckList[0].querySelector('cr-toggle').disabled);
assertTrue(spellCheckList[1].querySelector('cr-toggle').disabled);
assertEquals(
getComputedStyle(spellCheckList[0].querySelector('.name-with-error'))
.pointerEvents,
'none');
assertEquals(
getComputedStyle(spellCheckList[1].querySelector('.name-with-error'))
.pointerEvents,
'none');
});
test('does not add a language without spellcheck support', () => {
const spellCheckLanguagesCount = spellCheckList.length;
// Enabling a language without spellcheck support should not add it to
// the list
languageHelper.enableLanguage('tk');
Polymer.dom.flush();
assertEquals(spellCheckList.length, spellCheckLanguagesCount);
});
test('toggle is disabled when there is no supported languages', () => {
assertFalse(spellCheckToggle.disabled);
// Empty out supported languages
languageHelper.setPrefValue('settings.language.preferred_languages', '');
assertTrue(spellCheckToggle.disabled);
assertFalse(spellCheckToggle.checked);
});
});
}); });
...@@ -19,6 +19,7 @@ cr.define('settings', function() { ...@@ -19,6 +19,7 @@ cr.define('settings', function() {
'recordAddLanguages', 'recordAddLanguages',
'recordManageInputMethods', 'recordManageInputMethods',
'recordToggleShowInputOptionsOnShelf', 'recordToggleShowInputOptionsOnShelf',
'recordToggleSpellCheck',
'recordToggleTranslate', 'recordToggleTranslate',
'recordAddInputMethod', 'recordAddInputMethod',
]); ]);
...@@ -44,6 +45,11 @@ cr.define('settings', function() { ...@@ -44,6 +45,11 @@ cr.define('settings', function() {
this.methodCalled('recordToggleShowInputOptionsOnShelf', value); this.methodCalled('recordToggleShowInputOptionsOnShelf', value);
} }
/** @override */
recordToggleSpellCheck(value) {
this.methodCalled('recordToggleSpellCheck', value);
}
/** @override */ /** @override */
recordToggleTranslate(value) { recordToggleTranslate(value) {
this.methodCalled('recordToggleTranslate', value); this.methodCalled('recordToggleTranslate', value);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment