chromium/ash/webui/common/resources/cr_elements/cr_radio_button/cr_radio_button_mixin.ts

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Mixin for cr-radio-button-like elements.
 * Forked from
 * ui/webui/resources/cr_elements/cr_radio_button/cr_radio_button_mixin.ts
 */

// clang-format off
import {dedupingMixin, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assert, assertNotReached} from '//resources/js/assert.js';

interface PaperRippleElement {
  clear(): void;
  showAndHoldDown(): void;
}

type Constructor<T> = new (...args: any[]) => T;

export const CrRadioButtonMixin = dedupingMixin(
    <T extends Constructor<PolymerElement>>(superClass: T): T&
    Constructor<CrRadioButtonMixinInterface> => {
      class CrRadioButtonMixin extends superClass implements
          CrRadioButtonMixinInterface {
        static get properties() {
          return {
            checked: {
              type: Boolean,
              value: false,
              reflectToAttribute: true,
            },

            disabled: {
              type: Boolean,
              value: false,
              reflectToAttribute: true,
              notify: true,
            },

            /**
             * Whether the radio button should be focusable or not. Toggling
             * this property sets the corresponding tabindex of the button
             * itself as well as any links in the button description.
             */
            focusable: {
              type: Boolean,
              value: false,
              observer: 'onFocusableChanged_',
            },

            hideLabelText: {
              type: Boolean,
              value: false,
              reflectToAttribute: true,
            },

            label: {
              type: String,
              value: '',  // Allows hidden$= binding to run without being set.
            },

            name: {
              type: String,
              notify: true,
              reflectToAttribute: true,
            },

            /**
             * Holds the tabIndex for the radio button.
             */
            buttonTabIndex_: {
              type: Number,
              computed: 'getTabIndex_(focusable)',
            },
          };
        }

        checked: boolean;
        disabled: boolean;
        focusable: boolean;
        hideLabelText: boolean;
        label: string;
        name: string;
        private buttonTabIndex_: number;

        override connectedCallback() {
          super.connectedCallback();
          this.addEventListener('blur', this.hideRipple_.bind(this));
          if (!document.documentElement.hasAttribute('chrome-refresh-2023')) {
            this.addEventListener('focus', this.onFocus_.bind(this));
          }
          this.addEventListener('up', this.hideRipple_.bind(this));
        }

        override focus() {
          const button = this.shadowRoot!.querySelector<HTMLElement>('#button');
          assert(button);
          button.focus();
        }

        getPaperRipple(): PaperRippleElement {
          assertNotReached();
        }

        private onFocus_() {
          this.getPaperRipple().showAndHoldDown();
        }

        private hideRipple_() {
          this.getPaperRipple().clear();
        }

        private onFocusableChanged_() {
          const links = this.querySelectorAll('a');
          links.forEach((link) => {
            // Remove the tab stop on any links when the row is unchecked.
            // Since the row is not tabbable, any links within the row
            // should not be either.
            link.tabIndex = this.checked ? 0 : -1;
          });
        }

        private getAriaChecked_(): string {
          return this.checked ? 'true' : 'false';
        }

        private getAriaDisabled_(): string {
          return this.disabled ? 'true' : 'false';
        }

        private getTabIndex_(): number {
          return this.focusable ? 0 : -1;
        }

        /**
         * When shift-tab is pressed, first bring the focus to the host
         * element. This accomplishes 2 things:
         * 1) Host doesn't get focused when the browser moves the focus
         *    backward.
         * 2) focus now escaped the shadow-dom of this element, so that
         *    it'll correctly obey non-zero tabindex ordering of the
         *    containing document.
         */
        private onInputKeydown_(e: KeyboardEvent) {
          if (e.shiftKey && e.key === 'Tab') {
            this.focus();
          }
        }
      }

      return CrRadioButtonMixin;
    });

export interface CrRadioButtonMixinInterface {
  checked: boolean;
  disabled: boolean;
  focusable: boolean;
  hideLabelText: boolean;
  label: string;
  name: string;

  getPaperRipple(): PaperRippleElement;
}