Commit 48bdf987 authored by Esmael El-Moslimany's avatar Esmael El-Moslimany Committed by Commit Bot

WebUI: create cr-radio-group, a replacement for paper-radio-group

Bug: 888922
Cq-Include-Trybots: luci.chromium.try:closure_compilation
Change-Id: Icb6a31e501a5fbad60faa06cf8577c5212624e96
Reviewed-on: https://chromium-review.googlesource.com/c/1265090
Commit-Queue: Esmael El-Moslimany <aee@chromium.org>
Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Cr-Commit-Position: refs/heads/master@{#602059}
parent d87f46c2
......@@ -10,39 +10,14 @@ Polymer({
CrRadioButtonBehavior,
],
properties: {
disabled: {
type: Boolean,
computed: 'computeDisabled_(pref.*)',
reflectToAttribute: true,
observer: 'disabledChanged_',
},
name: {
type: String,
notify: true,
},
},
/**
* @return {boolean} Whether the button is disabled.
* @private
*/
computeDisabled_: function() {
return this.pref.enforcement == chrome.settingsPrivate.Enforcement.ENFORCED;
},
/**
* @param {boolean} current
* @param {boolean} previous
* @private
*/
disabledChanged_: function(current, previous) {
if (previous === undefined && !this.disabled)
return;
observers: [
'updateDisabled_(pref.enforcement)',
],
this.setAttribute('tabindex', this.disabled ? -1 : 0);
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
/** @private */
updateDisabled_: function() {
this.disabled =
this.pref.enforcement == chrome.settingsPrivate.Enforcement.ENFORCED;
},
/**
......
......@@ -353,6 +353,31 @@ TEST_F('CrElementsRadioButtonTest', 'All', function() {
mocha.run();
});
/**
* @constructor
* @extends {CrElementsBrowserTest}
*/
function CrElementsRadioGroupTest() {}
CrElementsRadioGroupTest.prototype = {
__proto__: CrElementsBrowserTest.prototype,
/** @override */
browsePreload:
'chrome://resources/cr_elements/cr_radio_group/cr_radio_group.html',
/** @override */
extraLibraries: CrElementsBrowserTest.prototype.extraLibraries.concat([
'../settings/test_util.js',
'cr_radio_group_test.js',
]),
};
TEST_F('CrElementsRadioGroupTest', 'All', function() {
mocha.run();
});
/**
* @constructor
* @extends {CrElementsBrowserTest}
......
......@@ -33,7 +33,6 @@ suite('cr-radio-button', function() {
}
function assertDisabled() {
assertEquals('-1', radioButton.getAttribute('tabindex'));
assertTrue(radioButton.hasAttribute('disabled'));
assertEquals('true', radioButton.getAttribute('aria-disabled'));
assertEquals('none', getComputedStyle(radioButton).pointerEvents);
......@@ -41,7 +40,6 @@ suite('cr-radio-button', function() {
}
function assertNotDisabled() {
assertEquals('0', radioButton.getAttribute('tabindex'));
assertFalse(radioButton.hasAttribute('disabled'));
assertEquals('false', radioButton.getAttribute('aria-disabled'));
assertEquals('1', getComputedStyle(radioButton).opacity);
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
suite('cr-radio-group', () => {
let radioGroup;
/** @override */
suiteSetup(() => {
return PolymerTest.importHtml(
'chrome://resources/cr_elements/cr_radio_button/cr_radio_button.html');
});
setup(() => {
document.body.innerHTML = `
<cr-radio-group>
<cr-radio-button name="1"></cr-radio-button>
<cr-radio-button name="2"></cr-radio-button>
<cr-radio-button name="3"><a></a></cr-radio-button>
</cr-radio-group>`;
radioGroup = document.body.querySelector('cr-radio-group');
Polymer.dom.flush();
});
/**
* @param {number} length
* @param {string} selector
*/
function checkLength(length, selector) {
assertEquals(length, radioGroup.querySelectorAll(selector).length);
}
/**
* @param {string} name
*/
function noneSelectedOneFocusable(name) {
checkLength(1, `:not([checked])[tabindex="0"][name="${name}"]`);
checkLength(2, ':not([checked])[tabindex="-1"]');
}
/**
* @param {string} key
* @param {Element=} target
*/
function press(key, target) {
MockInteractions.pressAndReleaseKeyOn(target || radioGroup, -1, [], key);
}
/**
* @param {!Array<string>} keys
* @param {number} initialSelection
* @param {number} selections
*/
function checkPressed(keys, initialSelection, expectedSelected) {
keys.forEach((key, i) => {
radioGroup.selected = `${initialSelection}`;
press(key);
checkSelected(expectedSelected);
});
}
/**
* @param {number} name
*/
function checkSelected(name) {
assertEquals(`${name}`, radioGroup.selected);
checkLength(1, `[name="${name}"][checked][tabindex="0"]`);
checkLength(2, `:not([name="${name}"]):not([checked])[tabindex="-1"]`);
}
test('selected-changed bubbles', () => {
const whenFired = test_util.eventToPromise('selected-changed', radioGroup);
radioGroup.selected = '1';
return whenFired;
});
test('key events when initially nothing checked', () => {
press('Enter');
checkSelected(1);
radioGroup.selected = '';
noneSelectedOneFocusable(1);
press(' ');
checkSelected(1);
radioGroup.selected = '';
noneSelectedOneFocusable(1);
press('ArrowRight');
checkSelected(2);
});
test('key events when an item is checked', () => {
checkPressed(['End'], 1, 3);
checkPressed(['Home'], 3, 1);
// Check for decrement.
checkPressed(['Home', 'PageUp', 'ArrowUp', 'ArrowLeft'], 2, 1);
// No change when reached first selected.
checkPressed(['Home', 'PageUp', 'ArrowUp', 'ArrowLeft'], 1, 1);
// Check for increment.
checkPressed(['End', 'ArrowRight', 'PageDown', 'ArrowDown'], 2, 3);
// No change when reached last selected.
checkPressed(['End', 'ArrowRight', 'PageDown', 'ArrowDown'], 3, 3);
});
test('mouse event', () => {
assertEquals('', radioGroup.selected);
radioGroup.querySelector('[name="2"]').click();
checkSelected(2);
});
test('key events skip over disabled radios', () => {
checkLength(1, '[tabindex="0"][name="1"]');
noneSelectedOneFocusable(1);
radioGroup.querySelector('[name="2"]').disabled = true;
press('PageDown');
checkSelected(3);
});
test('disabled makes radios not focusable', () => {
radioGroup.selected = '1';
checkSelected(1);
radioGroup.disabled = true;
checkLength(3, '[tabindex="-1"]');
radioGroup.disabled = false;
checkSelected(1);
const firstRadio = radioGroup.querySelector('[name="1"]');
firstRadio.disabled = true;
checkLength(2, '[tabindex="-1"]');
checkLength(1, '[tabindex="0"][name="2"]');
firstRadio.disabled = false;
checkSelected(1);
radioGroup.selected = '';
noneSelectedOneFocusable(1);
firstRadio.disabled = true;
noneSelectedOneFocusable(2);
});
test('radios name change updates selection and tabindex', () => {
radioGroup.selected = '1';
checkSelected(1);
radioGroup.querySelector('[name="1"]').name = 'A';
checkLength(1, ':not([checked])[tabindex="0"][name="A"]');
checkLength(2, '[tabindex="-1"]');
const secondRadio = radioGroup.querySelector('[name="2"]');
radioGroup.querySelector('[name="2"]').name = '1';
checkLength(1, '[checked][tabindex="0"][name="1"]');
checkLength(2, '[tabindex="-1"]');
});
test('radios with links', () => {
const a = radioGroup.querySelector('a');
assertTrue(!!a);
noneSelectedOneFocusable(1);
press('Enter', a);
press(' ', a);
a.click();
noneSelectedOneFocusable(1);
radioGroup.querySelector('[name="1"]').click();
checkSelected(1);
press('Enter', a);
press(' ', a);
a.click();
checkSelected(1);
});
});
......@@ -60,6 +60,7 @@ CrElementsPolicyPrefIndicatorTest.*
CrElementsProfileAvatarSelectorFocusTest.*
CrElementsProfileAvatarSelectorTest.*
CrElementsRadioButtonTest.*
CrElementsRadioGroupTest.*
CrElementsScrollableBehaviorTest.*
CrElementsSearchableDropDownTest.*
CrElementsSliderTest.*
......
......@@ -81,4 +81,4 @@ class ResizeObserver {
* @param {...*} args The function arguments.
* TODO(rbpotter): Remove this once it is added to Closure Compiler itself.
*/
Polymer.RenderStatus.beforeNextRender = function(element, fn, args) {}
Polymer.RenderStatus.beforeNextRender = function(element, fn, args) {};
......@@ -18,6 +18,7 @@ group("closure_compile") {
"cr_link_row:closure_compile",
"cr_profile_avatar_selector:closure_compile",
"cr_radio_button:closure_compile",
"cr_radio_group:closure_compile",
"cr_searchable_drop_down:closure_compile",
"cr_slider:closure_compile",
"cr_toast:closure_compile",
......
......@@ -20,6 +20,7 @@ var CrRadioButtonBehaviorImpl = {
type: Boolean,
value: false,
reflectToAttribute: true,
notify: true,
observer: 'disabledChanged_',
},
......@@ -27,6 +28,12 @@ var CrRadioButtonBehaviorImpl = {
type: String,
value: '', // Allows the hidden$= binding to run without being set.
},
name: {
type: String,
notify: true,
reflectToAttribute: true,
},
},
listeners: {
......@@ -41,7 +48,6 @@ var CrRadioButtonBehaviorImpl = {
'aria-disabled': 'false',
'aria-checked': 'false',
role: 'radio',
tabindex: 0,
},
/** @private */
......@@ -58,7 +64,8 @@ var CrRadioButtonBehaviorImpl = {
if (previous === undefined && !this.disabled)
return;
this.setAttribute('tabindex', this.disabled ? -1 : 0);
if (this.disabled)
this.setAttribute('tabindex', '-1');
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
},
......
# Copyright 2018 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/compile_js.gni")
js_type_check("closure_compile") {
deps = [
":cr_radio_group",
]
}
js_library("cr_radio_group") {
deps = [
"//ui/webui/resources/js:event_tracker",
]
externs_list = [ "$externs_path/pending.js" ]
}
<link rel="import" href="../../html/polymer.html">
<link rel="import" href="../../html/event_tracker.html">
<link rel="import" href="../shared_vars_css.html">
<dom-module id="cr-radio-group">
<template>
<style>
:host {
display: inline-block;
}
:host ::slotted(*) {
padding: var(--cr-radio-group-item-padding, 12px);
}
:host([disabled]) {
cursor: initial;
pointer-events: none;
user-select: none;
}
:host([disabled]) ::slotted(*) {
opacity: var(--cr-disabled-opacity);
}
</style>
<slot></slot>
</template>
<script src="cr_radio_group.js"></script>
</dom-module>
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(() => {
/**
* @param {!Element} radio
* @return {boolean}
*/
function isEnabled(radio) {
return radio.matches(':not([disabled]):not([hidden])') &&
radio.style.display != 'none' && radio.style.visibility != 'hidden';
}
/**
* @param {!EventTarget} target
* @return {boolean}
*/
function isRadioButton(target) {
return /^(cr|controlled)-radio-button$/i.test(target.tagName);
}
Polymer({
is: 'cr-radio-group',
properties: {
disabled: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
selected: {
type: String,
value: '',
notify: true,
},
},
listeners: {
keydown: 'onKeyDown_',
click: 'onClick_',
},
observers: [
'update_(disabled, selected)',
],
hostAttributes: {
role: 'radiogroup',
},
/** @private {Array<!Element>} */
buttons_: null,
/** @private {EventTracker} */
buttonEventTracker_: null,
/** @private {Map<string, number>} */
deltaKeyMap_: null,
/** @private {boolean} */
isRtl_: false,
/** @private {PolymerDomApi.ObserveHandle} */
observer_: null,
/** @private {Function} */
populateBound_: null,
/** @override */
attached: function() {
this.isRtl_ = this.matches(':host-context([dir=rtl]) cr-slider');
this.deltaKeyMap_ = new Map([
['ArrowDown', 1],
['ArrowLeft', this.isRtl_ ? 1 : -1],
['ArrowRight', this.isRtl_ ? -1 : 1],
['ArrowUp', -1],
['PageDown', 1],
['PageUp', -1],
]);
this.buttonEventTracker_ = new EventTracker();
this.populateBound_ = () => this.populate_();
// Needed for when the radio buttons change when using dom-repeat or
// dom-if.
// TODO(crbug.com/738611): After migration to Polymer 2, remove Polymer 1
// references.
if (Polymer.DomIf)
this.$$('slot').addEventListener('slotchange', this.populateBound_);
else
this.observer_ = Polymer.dom(this).observeNodes(this.populateBound_);
this.populate_();
},
/** @override */
detached: function() {
if (Polymer.DomIf)
this.$$('slot').removeEventListener('slotchange', this.populateBound_);
else if (this.observer_) {
Polymer.dom(this).unobserveNodes(
/** @type {!PolymerDomApi.ObserveHandle} */ (this.observer_));
}
this.buttonEventTracker_.removeAll();
},
/** @override */
focus: function() {
if (!this.disabled) {
const radio =
this.buttons_.find(radio => radio.getAttribute('tabindex') == '0');
if (radio)
radio.focus();
}
},
/**
* @param {!KeyboardEvent} event
* @private
*/
onKeyDown_: function(event) {
if (event.path.some(target => /^(a|(cr-|)input)$/i.test(target.tagName)))
return;
if (this.disabled || event.ctrlKey || event.shiftKey || event.metaKey ||
event.altKey) {
return;
}
const enabledRadios = this.buttons_.filter(isEnabled);
if (enabledRadios.length == 0)
return;
const lastSelection = enabledRadios.findIndex(radio => radio.checked);
let selectedIndex;
const max = enabledRadios.length - 1;
if (lastSelection == -1 && (event.key == ' ' || event.key == 'Enter')) {
selectedIndex = 0;
} else if (event.key == 'Home') {
selectedIndex = 0;
} else if (event.key == 'End') {
selectedIndex = max;
} else if (this.deltaKeyMap_.has(event.key)) {
const delta = this.deltaKeyMap_.get(event.key);
// If nothing selected, start from the first radio then add |delta|.
selectedIndex = Math.max(0, lastSelection) + delta;
selectedIndex = Math.min(max, Math.max(0, selectedIndex));
} else {
return;
}
const radio = enabledRadios[selectedIndex];
const name = `${radio.name}`;
if (this.selected != name) {
event.preventDefault();
this.selected = name;
radio.focus();
}
},
/**
* @param {!Event} event
* @private
*/
onClick_: function(event) {
if (event.path.some(target => /^a$/i.test(target.tagName)))
return;
const target = event.path.find(isRadioButton);
const name = `${target.name}`;
if (target && !target.disabled && this.selected != name)
this.selected = name;
},
/** @private */
populate_: function() {
// TODO(crbug.com/738611): After migration to Polymer 2, remove
// Polymer 1 references.
this.buttons_ = Polymer.DomIf ?
this.$$('slot').assignedNodes({flatten: true}).filter(isRadioButton) :
this.queryAllEffectiveChildren(
'cr-radio-button, controlled-radio-button');
this.buttonEventTracker_.removeAll();
this.buttons_.forEach(el => {
this.buttonEventTracker_.add(
el, 'disabled-changed', () => this.populate_());
this.buttonEventTracker_.add(
el, 'name-changed', () => this.populate_());
});
this.update_();
},
/** @private */
update_: function() {
if (!this.buttons_ || this.selected == undefined)
return;
let noneMadeFocusable = true;
this.buttons_.forEach(radio => {
radio.checked = radio.name == this.selected;
const canBeFocused =
radio.checked && !this.disabled && isEnabled(radio);
noneMadeFocusable &= !canBeFocused;
radio.setAttribute('tabindex', canBeFocused ? '0' : '-1');
});
if (noneMadeFocusable && !this.disabled) {
const focusable = this.buttons_.find(isEnabled);
if (focusable)
focusable.setAttribute('tabindex', '0');
}
},
});
})();
......@@ -110,6 +110,14 @@
file="cr_elements/cr_radio_button/cr_radio_button_style_css.html"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_RADIO_GROUP_HTML"
file="cr_elements/cr_radio_group/cr_radio_group.html"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_RADIO_GROUP_JS"
file="cr_elements/cr_radio_group/cr_radio_group.js"
type="chrome_html"
compress="gzip" />
<structure name="IDR_CR_ELEMENTS_CR_SLIDER_HTML"
file="cr_elements/cr_slider/cr_slider.html"
type="chrome_html"
......
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