chromium/chrome/browser/resources/bookmarks/dialog_focus_manager.ts

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

import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';

import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import {assert} from 'chrome://resources/js/assert.js';

/**
 * Manages focus restoration for modal dialogs. After the final dialog in a
 * stack is closed, restores focus to the element which was focused when the
 * first dialog was opened.
 */
export class DialogFocusManager {
  private previousFocusElement_: HTMLElement|null = null;
  private dialogs_: Set<HTMLDialogElement|CrDialogElement> = new Set();

  showDialog(dialog: (HTMLDialogElement|CrDialogElement), showFn?: () => void) {
    if (!showFn) {
      showFn = function() {
        dialog.showModal();
      };
    }

    // Update the focus if there are no open dialogs or if this is the only
    // dialog and it's getting reshown.
    if (!this.dialogs_.size ||
        (this.dialogs_.has(dialog) && this.dialogs_.size === 1)) {
      this.updatePreviousFocus_();
    }

    if (!this.dialogs_.has(dialog)) {
      dialog.addEventListener('close', this.getCloseListener_(dialog));
      this.dialogs_.add(dialog);
    }

    showFn();
  }

  /**
   * @return True if the document currently has an open dialog.
   */
  hasOpenDialog(): boolean {
    return this.dialogs_.size > 0;
  }

  /**
   * Clears the stored focus element, so that focus does not restore when all
   * dialogs are closed.
   */
  clearFocus() {
    this.previousFocusElement_ = null;
  }

  private updatePreviousFocus_() {
    this.previousFocusElement_ = this.getFocusedElement_();
  }

  private getFocusedElement_(): HTMLElement {
    let focus = document.activeElement as HTMLElement;
    while (focus.shadowRoot && focus.shadowRoot!.activeElement) {
      focus = focus.shadowRoot!.activeElement as HTMLElement;
    }

    return focus;
  }

  private getCloseListener_(dialog: (HTMLDialogElement|CrDialogElement)):
      ((p1: Event) => void) {
    const closeListener = (_e: Event) => {
      // If the dialog is open, then it got reshown immediately and we
      // shouldn't clear it until it is closed again.
      if (dialog.open) {
        return;
      }

      assert(this.dialogs_.delete(dialog));
      // Focus the originally focused element if there are no more dialogs.
      if (!this.hasOpenDialog() && this.previousFocusElement_) {
        this.previousFocusElement_.focus();
      }

      dialog.removeEventListener('close', closeListener);
    };

    return closeListener;
  }

  static getInstance(): DialogFocusManager {
    return instance || (instance = new DialogFocusManager());
  }

  static setInstance(obj: DialogFocusManager|null) {
    instance = obj;
  }
}

let instance: DialogFocusManager|null = null;