| // Copyright 2016 The Chromium Authors | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | /** | 
 |  * @fileoverview | 
 |  * `settings-toggle-button` is a toggle that controls a supplied preference. | 
 |  */ | 
 | import '//resources/cr_elements/cr_actionable_row_style.css.js'; | 
 | import '//resources/cr_elements/cr_shared_style.css.js'; | 
 | import '//resources/cr_elements/cr_shared_vars.css.js'; | 
 | import '//resources/cr_elements/action_link.css.js'; | 
 | import '//resources/cr_elements/cr_toggle/cr_toggle.js'; | 
 | import '/shared/settings/controls/cr_policy_pref_indicator.js'; | 
 | import '//resources/cr_elements/cr_icon/cr_icon.js'; | 
 |  | 
 | import type {CrToggleElement} from '//resources/cr_elements/cr_toggle/cr_toggle.js'; | 
 | import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; | 
 | import {SettingsBooleanControlMixin} from '/shared/settings/controls/settings_boolean_control_mixin.js'; | 
 | import {assert} from 'chrome://resources/js/assert.js'; | 
 | import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js'; | 
 |  | 
 | import {getTemplate} from './settings_toggle_button.html.js'; | 
 |  | 
 |  | 
 | export interface SettingsToggleButtonElement { | 
 |   $: { | 
 |     control: CrToggleElement, | 
 |     labelWrapper: HTMLElement, | 
 |   }; | 
 | } | 
 |  | 
 | const SettingsToggleButtonElementBase = | 
 |     SettingsBooleanControlMixin(PolymerElement); | 
 |  | 
 | export class SettingsToggleButtonElement extends | 
 |     SettingsToggleButtonElementBase { | 
 |   static get is() { | 
 |     return 'settings-toggle-button'; | 
 |   } | 
 |  | 
 |   static get template() { | 
 |     return getTemplate(); | 
 |   } | 
 |  | 
 |   static get properties() { | 
 |     return { | 
 |       ariaLabel: { | 
 |         type: String, | 
 |         reflectToAttribute: false,  // Handled by #control. | 
 |         observer: 'onAriaLabelSet_', | 
 |         value: '', | 
 |       }, | 
 |  | 
 |       ariaShowLabel: { | 
 |         type: Boolean, | 
 |         reflectToAttribute: true, | 
 |         value: false, | 
 |       }, | 
 |  | 
 |       ariaShowSublabel: { | 
 |         type: Boolean, | 
 |         reflectToAttribute: true, | 
 |         value: false, | 
 |       }, | 
 |  | 
 |       elideLabel: { | 
 |         type: Boolean, | 
 |         reflectToAttribute: true, | 
 |       }, | 
 |  | 
 |       learnMoreUrl: { | 
 |         type: String, | 
 |         reflectToAttribute: true, | 
 |       }, | 
 |  | 
 |       subLabelWithLink: { | 
 |         type: String, | 
 |         reflectToAttribute: true, | 
 |       }, | 
 |  | 
 |       learnMoreAriaLabel: { | 
 |         type: String, | 
 |         value: '', | 
 |       }, | 
 |  | 
 |       icon: String, | 
 |  | 
 |       subLabelIcon: String, | 
 |  | 
 |       /** | 
 |        * If true, the host element does not get a click event handler and the | 
 |        * client is responsible for determining their own click logic. Thus when | 
 |        * true, clicking on the setting row does not toggle the setting pref. | 
 |        * Note, this boolean is only used on ready() callback, and any changes | 
 |        * after that have no effect. | 
 |        */ | 
 |       noToggleOnHostClick: { | 
 |         type: Boolean, | 
 |         value: false, | 
 |       }, | 
 |     }; | 
 |   } | 
 |  | 
 |   static get observers() { | 
 |     return [ | 
 |       'onDisableOrPrefChange_(disabled, pref.*)', | 
 |     ]; | 
 |   } | 
 |  | 
 |   declare ariaLabel: string; | 
 |   declare ariaShowLabel: boolean; | 
 |   declare ariaShowSublabel: boolean; | 
 |   declare elideLabel: boolean; | 
 |   declare icon: string; | 
 |   declare learnMoreAriaLabel: string; | 
 |   declare learnMoreUrl: string; | 
 |   declare subLabelWithLink: string; | 
 |   declare subLabelIcon: string; | 
 |   declare noToggleOnHostClick: boolean; | 
 |  | 
 |   override ready() { | 
 |     super.ready(); | 
 |  | 
 |     // If the settings toggle is noToggleOnHostClick then do not update the | 
 |     // setting pref on click. Instead let parent code use a custom click | 
 |     // handler as needed. | 
 |     if (!this.noToggleOnHostClick) { | 
 |       this.addEventListener('click', this.onHostClick_); | 
 |     } | 
 |   } | 
 |  | 
 |   private fire_(eventName: string, detail?: any) { | 
 |     this.dispatchEvent( | 
 |         new CustomEvent(eventName, {detail, bubbles: true, composed: true})); | 
 |   } | 
 |  | 
 |   override focus() { | 
 |     this.$.control.focus(); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Removes the aria-label attribute if it's added by $i18n{...}. | 
 |    */ | 
 |   private onAriaLabelSet_() { | 
 |     if (this.hasAttribute('aria-label')) { | 
 |       const ariaLabel = this.ariaLabel; | 
 |       this.removeAttribute('aria-label'); | 
 |       this.ariaLabel = ariaLabel; | 
 |     } | 
 |   } | 
 |  | 
 |   private getAriaLabel_(): string { | 
 |     return this.ariaLabel || this.label; | 
 |   } | 
 |  | 
 |   private getLearnMoreAriaLabelledBy_(): string { | 
 |     return this.learnMoreAriaLabel ? 'learn-more-aria-label' : | 
 |                                      'sub-label-text learn-more'; | 
 |   } | 
 |  | 
 |   getBubbleAnchor() { | 
 |     const anchor = this.shadowRoot!.querySelector<HTMLElement>('#control'); | 
 |     assert(anchor); | 
 |     return anchor; | 
 |   } | 
 |  | 
 |   private onDisableOrPrefChange_() { | 
 |     this.toggleAttribute('effectively-disabled_', this.controlDisabled()); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Handles non cr-toggle button clicks (cr-toggle handles its own click events | 
 |    * which don't bubble). | 
 |    */ | 
 |   private onHostClick_(e: Event) { | 
 |     assert(!this.noToggleOnHostClick); | 
 |     e.stopPropagation(); | 
 |     if (this.controlDisabled()) { | 
 |       return; | 
 |     } | 
 |     this.updateCheckedAndNotify_(!this.checked); | 
 |   } | 
 |  | 
 |   private onLearnMoreClick_(e: CustomEvent<boolean>) { | 
 |     e.stopPropagation(); | 
 |     this.fire_('learn-more-clicked'); | 
 |   } | 
 |  | 
 |   /** | 
 |    * Set up the contents of sub label with link. | 
 |    */ | 
 |   private getSubLabelWithLinkContent_(contents: string) { | 
 |     return sanitizeInnerHtml(contents, { | 
 |       attrs: [ | 
 |         'id', | 
 |         'is', | 
 |         'aria-description', | 
 |         'aria-hidden', | 
 |         'aria-label', | 
 |         'aria-labelledby', | 
 |         'tabindex', | 
 |       ], | 
 |     }); | 
 |   } | 
 |  | 
 |   private onSubLabelTextWithLinkClick_(e: Event) { | 
 |     const target = e.target as HTMLElement; | 
 |     if (target.tagName === 'A') { | 
 |       this.fire_('sub-label-link-clicked', target.id); | 
 |       e.preventDefault(); | 
 |       e.stopPropagation(); | 
 |     } | 
 |   } | 
 |  | 
 |   private onChange_(e: CustomEvent<boolean>) { | 
 |     // Prevent cr-toggle's change event from propagating to the parent since | 
 |     // this element fires its own 'change' event. | 
 |     e.stopPropagation(); | 
 |     this.updateCheckedAndNotify_(e.detail); | 
 |   } | 
 |  | 
 |   private updateCheckedAndNotify_(checked: boolean) { | 
 |     this.checked = checked; | 
 |     this.notifyChangedByUserInteraction(); | 
 |     this.fire_('change', this.checked); | 
 |   } | 
 | } | 
 |  | 
 | declare global { | 
 |   interface HTMLElementTagNameMap { | 
 |     'settings-toggle-button': SettingsToggleButtonElement; | 
 |   } | 
 | } | 
 |  | 
 | customElements.define( | 
 |     SettingsToggleButtonElement.is, SettingsToggleButtonElement); |