| // 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'; |
| } |
| |
| Polymer({ |
| is: 'cr-radio-group', |
| |
| properties: { |
| disabled: { |
| type: Boolean, |
| value: false, |
| reflectToAttribute: true, |
| }, |
| |
| selected: { |
| type: String, |
| notify: true, |
| }, |
| |
| selectable: { |
| type: String, |
| value: 'cr-radio-button, controlled-radio-button', |
| }, |
| |
| /** |
| * @type {!RegExp} |
| * @private |
| */ |
| selectableRegExp_: { |
| value: Object, |
| computed: 'computeSelectableRegExp_(selectable)', |
| }, |
| }, |
| |
| 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-radio-group'); |
| 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 || !this.buttons_) |
| return; |
| |
| const radio = |
| this.buttons_.find(radio => radio.getAttribute('tabindex') == '0'); |
| if (radio) |
| radio.focus(); |
| }, |
| |
| /** |
| * @param {!KeyboardEvent} event |
| * @private |
| */ |
| onKeyDown_: function(event) { |
| if (this.disabled) |
| return; |
| |
| if (event.ctrlKey || event.shiftKey || event.metaKey || event.altKey) |
| return; |
| |
| const targetElement = /** @type {!Element} */ (event.target); |
| if (!this.buttons_.includes(targetElement)) |
| return; |
| |
| if (event.key == ' ' || event.key == 'Enter') { |
| event.preventDefault(); |
| this.select_(/** @type {!Element} */ (event.target)); |
| return; |
| } |
| |
| const enabledRadios = this.buttons_.filter(isEnabled); |
| if (enabledRadios.length == 0) |
| return; |
| |
| let selectedIndex; |
| const max = enabledRadios.length - 1; |
| 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|. |
| const lastSelection = enabledRadios.findIndex(radio => radio.checked); |
| 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(); |
| } |
| }, |
| |
| /** |
| * @return {!RegExp} |
| * @private |
| */ |
| computeSelectableRegExp_: function() { |
| const tags = this.selectable.split(', ').join('|'); |
| return new RegExp(`^(${tags})$`, 'i'); |
| }, |
| |
| /** |
| * @param {!Event} event |
| * @private |
| */ |
| onClick_: function(event) { |
| const path = event.composedPath(); |
| if (path.some(target => /^a$/i.test(target.tagName))) |
| return; |
| const target = /** @type {!Element} */ ( |
| path.find(n => this.selectableRegExp_.test(n.tagName))); |
| if (target && this.buttons_.includes(target)) |
| this.select_(/** @type {!Element} */ (target)); |
| }, |
| |
| /** @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(n => this.selectableRegExp_.test(n.tagName)) : |
| this.queryAllEffectiveChildren(this.selectable); |
| 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_(); |
| }, |
| |
| /** |
| * @param {!Element} button |
| * @private |
| */ |
| select_: function(button) { |
| if (!isEnabled(button)) |
| return; |
| |
| const name = `${button.name}`; |
| if (this.selected != name) |
| this.selected = name; |
| }, |
| |
| /** @private */ |
| update_: function() { |
| if (!this.buttons_) |
| return; |
| let noneMadeFocusable = true; |
| this.buttons_.forEach(radio => { |
| radio.checked = this.selected != undefined && |
| 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'); |
| } |
| }, |
| }); |
| })(); |