paper-dialog-behavior.html 7.22 KB
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../iron-overlay-behavior/iron-overlay-behavior.html">
<link rel="import" href="../paper-styles/paper-styles.html">

<script>

/**
Use `Polymer.PaperDialogBehavior` and `paper-dialog-common.css` to implement a Material Design
dialog.

For example, if `<paper-dialog-impl>` implements this behavior:

    <paper-dialog-impl>
        <h2>Header</h2>
        <div>Dialog body</div>
        <div class="buttons">
            <paper-button dialog-dismiss>Cancel</paper-button>
            <paper-button dialog-confirm>Accept</paper-button>
        </div>
    </paper-dialog-impl>

`paper-dialog-common.css` provide styles for a header, content area, and an action area for buttons.
Use the `<h2>` tag for the header and the `buttons` class for the action area. You can use the
`paper-dialog-scrollable` element (in its own repository) if you need a scrolling content area.

Use the `dialog-dismiss` and `dialog-confirm` attributes on interactive controls to close the
dialog. If the user dismisses the dialog with `dialog-confirm`, the `closingReason` will update
to include `confirmed: true`.

### Styling

The following custom properties and mixins are available for styling.

Custom property | Description | Default
----------------|-------------|----------
`--paper-dialog-background-color` | Dialog background color                     | `--primary-background-color`
`--paper-dialog-color`            | Dialog foreground color                     | `--primary-text-color`
`--paper-dialog`                  | Mixin applied to the dialog                 | `{}`
`--paper-dialog-title`            | Mixin applied to the title (`<h2>`) element | `{}`
`--paper-dialog-button-color`     | Button area foreground color                | `--default-primary-color`

### Accessibility

This element has `role="dialog"` by default. Depending on the context, it may be more appropriate
to override this attribute with `role="alertdialog"`.

If `modal` is set, the element will set `aria-modal` and prevent the focus from exiting the element.
It will also ensure that focus remains in the dialog.

The `aria-labelledby` attribute will be set to the header element, if one exists.

@hero hero.svg
@demo demo/index.html
@polymerBehavior Polymer.PaperDialogBehavior
*/

  Polymer.PaperDialogBehaviorImpl = {

    hostAttributes: {
      'role': 'dialog',
      'tabindex': '-1'
    },

    properties: {

      /**
       * If `modal` is true, this implies `no-cancel-on-outside-click` and `with-backdrop`.
       */
      modal: {
        observer: '_modalChanged',
        type: Boolean,
        value: false
      },

      /** @type {?Node} */
      _lastFocusedElement: {
        type: Object
      },

      _boundOnFocus: {
        type: Function,
        value: function() {
          return this._onFocus.bind(this);
        }
      },

      _boundOnBackdropClick: {
        type: Function,
        value: function() {
          return this._onBackdropClick.bind(this);
        }
      }

    },

    listeners: {
      'tap': '_onDialogClick',
      'iron-overlay-opened': '_onIronOverlayOpened',
      'iron-overlay-closed': '_onIronOverlayClosed'
    },

    attached: function() {
      this._observer = this._observe(this);
      this._updateAriaLabelledBy();
    },

    detached: function() {
      if (this._observer) {
        this._observer.disconnect();
      }
    },

    _observe: function(node) {
      var observer = new MutationObserver(function() {
        this._updateAriaLabelledBy();
      }.bind(this));
      observer.observe(node, {
        childList: true,
        subtree: true
      });
      return observer;
    },

    _modalChanged: function() {
      if (this.modal) {
        this.setAttribute('aria-modal', 'true');
      } else {
        this.setAttribute('aria-modal', 'false');
      }
      // modal implies noCancelOnOutsideClick and withBackdrop if true, don't overwrite
      // those properties otherwise.
      if (this.modal) {
        this.noCancelOnOutsideClick = true;
        this.withBackdrop = true;
      }
    },

    _updateAriaLabelledBy: function() {
      var header = Polymer.dom(this).querySelector('h2');
      if (!header) {
        this.removeAttribute('aria-labelledby');
        return;
      }
      var headerId = header.getAttribute('id');
      if (headerId && this.getAttribute('aria-labelledby') === headerId) {
        return;
      }
      // set aria-describedBy to the header element
      var labelledById;
      if (headerId) {
        labelledById = headerId;
      } else {
        labelledById = 'paper-dialog-header-' + new Date().getUTCMilliseconds();
        header.setAttribute('id', labelledById);
      }
      this.setAttribute('aria-labelledby', labelledById);
    },

    _updateClosingReasonConfirmed: function(confirmed) {
      this.closingReason = this.closingReason || {};
      this.closingReason.confirmed = confirmed;
    },

    _onDialogClick: function(event) {
      var target = event.target;
      while (target && target !== this) {
        if (target.hasAttribute) {
          if (target.hasAttribute('dialog-dismiss')) {
            this._updateClosingReasonConfirmed(false);
            this.close();
            break;
          } else if (target.hasAttribute('dialog-confirm')) {
            this._updateClosingReasonConfirmed(true);
            this.close();
            break;
          }
        }
        target = target.parentNode;
      }
    },

    _onIronOverlayOpened: function() {
      if (this.modal) {
        document.body.addEventListener('focus', this._boundOnFocus, true);
        this.backdropElement.addEventListener('click', this._boundOnBackdropClick);
      }
    },

    _onIronOverlayClosed: function() {
      document.body.removeEventListener('focus', this._boundOnFocus, true);
      this.backdropElement.removeEventListener('click', this._boundOnBackdropClick);
    },

    _onFocus: function(event) {
      if (this.modal) {
        var target = event.target;
        while (target && target !== this && target !== document.body) {
          target = target.parentNode;
        }
        if (target) {
          if (target === document.body) {
            if (this._lastFocusedElement) {
              this._lastFocusedElement.focus();
            } else {
              this._focusNode.focus();
            }
          } else {
            this._lastFocusedElement = event.target;
          }
        }
      }
    },

    _onBackdropClick: function() {
      if (this.modal) {
        if (this._lastFocusedElement) {
          this._lastFocusedElement.focus();
        } else {
          this._focusNode.focus();
        }
      }
    }

  };

  /** @polymerBehavior */
  Polymer.PaperDialogBehavior = [Polymer.IronOverlayBehavior, Polymer.PaperDialogBehaviorImpl];

</script>