chromium/chrome/browser/resources/ash/settings/common/deep_linking_mixin.ts

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

/**
 * @fileoverview Polymer mixin for scrolling/focusing/indicating
 * setting elements with deep links.
 */

import {afterNextRender, dedupingMixin, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getSettingIdParameter} from './setting_id_param_util.js';
import {Constructor} from './types.js';
import {Setting} from '../mojom-webui/setting.mojom-webui.js';

const DEEP_LINK_FOCUS_ID = 'deep-link-focus-id';

export interface DeepLinkingMixinInterface {
  Setting: Setting;
  supportedSettingIds: Set<Setting>;

  getDeepLinkSettingId(): Setting|null;
  showDeepLink(settingId: Setting):
      Promise<{deepLinkShown: boolean, pendingSettingId: Setting|null}>;
  showDeepLinkElement(elToFocus: HTMLElement): void;
  beforeDeepLinkAttempt(settingId: Setting): boolean;
  attemptDeepLink():
      Promise<{deepLinkShown: boolean, pendingSettingId: Setting|null}>;
}

export const DeepLinkingMixin = dedupingMixin(
    <T extends Constructor<PolymerElement>>(superClass: T): T&
    Constructor<DeepLinkingMixinInterface> => {
      class DeepLinkingMixinInternal extends superClass implements
          DeepLinkingMixinInterface {
        static get properties() {
          return {
            /**
             * An object whose values are the kSetting mojom values. Defined
             * as a property so that the Setting enum values can be used in
             * the element's HTML template to define deep link ID attributes.
             */
            Setting: {
              type: Object,
              value: Setting,
            },

            /**
             * Set of setting IDs that could be deep linked to. Initialized as
             * an empty set, should be overridden with applicable setting IDs.
             */
            supportedSettingIds: {
              type: Object,
              value: () => new Set<Setting>(),
            },
          };
        }

        // Disabling the naming convention rule since this property mirrors
        // the exact value of the Setting enum.
        // eslint-disable-next-line @typescript-eslint/naming-convention
        Setting: Setting;
        supportedSettingIds: Set<Setting>;

        /**
         * Retrieves the settingId saved in the url's query parameter. Returns
         * null if deep linking is disabled or if no settingId is found.
         */
        getDeepLinkSettingId(): Setting|null {
          const settingIdStr = getSettingIdParameter();
          if (!settingIdStr) {
            return null;
          }
          const settingIdNum = Number(settingIdStr);
          if (isNaN(settingIdNum)) {
            return null;
          }
          return settingIdNum as Setting;
        }

        /**
         * Focuses the deep linked element referred to by |settingId|. Returns a
         * Promise for an object that reflects if the deep link was shown or
         * not. The object has a boolean |deepLinkShown| and any
         * |pendingSettingId| that couldn't be shown.
         */
        showDeepLink(settingId: Setting):
            Promise<{deepLinkShown: boolean, pendingSettingId: Setting|null}> {
          return new Promise(resolve => {
            afterNextRender(this, () => {
              const elToFocus = this.shadowRoot!.querySelector<HTMLElement>(
                  `[${DEEP_LINK_FOCUS_ID}~="${settingId}"]`);
              if (!elToFocus || elToFocus.hidden) {
                console.warn(
                    `Element with deep link id ${settingId} not focusable.`);
                resolve({deepLinkShown: false, pendingSettingId: settingId});
                return;
              }

              this.showDeepLinkElement(elToFocus);
              resolve({deepLinkShown: true, pendingSettingId: settingId});
            });
          });
        }

        /**
         * Focuses the deep linked element |elem|. Returns whether the deep link
         * was shown or not.
         */
        showDeepLinkElement(elToFocus: HTMLOrSVGElement): void {
          elToFocus.focus();
        }

        /**
         * Override this method to execute code after a supported settingId is
         * found and before the deep link is shown. Returns whether or not the
         * deep link attempt should continue. Default behavior is to no op and
         * then return true, continuing the deep link attempt.
         */
        beforeDeepLinkAttempt(_settingId: Setting): boolean {
          return true;
        }

        /**
         * Checks if there are settingIds that can be linked to and attempts to
         * show the deep link. Returns a Promise for an object that reflects if
         * the deep link was shown or not. The object has a boolean
         * |deepLinkShown| and any |pendingSettingId| that couldn't be shown.
         */
        attemptDeepLink():
            Promise<{deepLinkShown: boolean, pendingSettingId: Setting|null}> {
          const settingId = this.getDeepLinkSettingId();
          // Explicitly check for null to handle settingId = 0.
          if (settingId === null || !this.supportedSettingIds.has(settingId)) {
            // No deep link was shown since the settingId was unsupported.
            return Promise.resolve(
                {deepLinkShown: false, pendingSettingId: null});
          }

          const shouldContinue = this.beforeDeepLinkAttempt(settingId);
          if (!shouldContinue) {
            // Don't continue the deep link attempt since it was presumably
            // handled manually in beforeDeepLinkAttempt().
            return Promise.resolve(
                {deepLinkShown: false, pendingSettingId: settingId});
          }

          return this.showDeepLink(settingId);
        }
      }

      return DeepLinkingMixinInternal;
    });