File size: 3,420 Bytes
da96562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<!--
  Matomo - free/libre analytics platform

  @link    https://matomo.org
  @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->

<template>
  <Teleport to="body">
    <div
      v-if="modelValue"
      class="modal-overlay matomo-modal-overlay open"
      @click="close"
    />
    <div
      v-show="modelValue"
      ref="root"
      class="modal matomo-modal"
      :class="modalClasses"
      role="dialog"
      aria-modal="true"
      :aria-label="ariaLabel"
      tabindex="-1"
    >
      <div class="modal-content matomo-modal-content" :class="contentClass">
        <slot></slot>
      </div>
      <div v-if="$slots.footer" class="modal-footer matomo-modal-footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </Teleport>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue';

type ClassValue = string | string[] | Record<string, boolean>;

/**
 * Vue-native modal shell. The forward direction for Matomo modals — the older
 * `MatomoDialog` (which wraps Materialize's `modalConfirm`) will be migrated
 * to this format in a follow-up and eventually removed.
 */
export default defineComponent({
  name: 'MatomoModal',
  props: {
    modelValue: {
      type: Boolean,
      required: true,
    },
    // Extra classes applied to the modal root, in the same shape Vue accepts
    // for `:class`. Use this to opt into modal-specific styling.
    classes: {
      type: [String, Array, Object] as PropType<ClassValue>,
      default: '',
    },
    // Extra classes applied to the inner `.modal-content` wrapper.
    contentClass: {
      type: [String, Array, Object] as PropType<ClassValue>,
      default: '',
    },
    ariaLabel: {
      type: String,
    },
  },
  emits: ['update:modelValue', 'opened', 'closed'],
  data() {
    return {
      previousBodyOverflow: '',
      previousFocus: null as HTMLElement | null,
    };
  },
  computed: {
    modalClasses(): Array<ClassValue> {
      return [{ open: this.modelValue }, this.classes];
    },
  },
  methods: {
    close() {
      if (!this.modelValue) {
        return;
      }
      this.$emit('update:modelValue', false);
    },

    onKeydown(event: KeyboardEvent) {
      if (event.key !== 'Escape') {
        return;
      }
      this.close();
    },

    activate() {
      const rootElement = this.$refs.root as HTMLElement;
      this.previousBodyOverflow = document.body.style.overflow;
      this.previousFocus = document.activeElement as HTMLElement | null;
      document.body.style.overflow = 'hidden';
      document.addEventListener('keydown', this.onKeydown);
      this.$nextTick(() => (rootElement).focus());
      this.$emit('opened', rootElement);
    },

    deactivate() {
      document.body.style.overflow = this.previousBodyOverflow;
      this.previousBodyOverflow = '';
      document.removeEventListener('keydown', this.onKeydown);
      if (this.previousFocus) {
        this.previousFocus.focus();
      }
      this.previousFocus = null;
      this.$emit('closed');
    },
  },
  watch: {
    modelValue(open: boolean, wasOpen: boolean) {
      if (open && !wasOpen) {
        this.activate();
      } else if (!open && wasOpen) {
        this.deactivate();
      }
    },
  },
  mounted() {
    if (this.modelValue) {
      this.activate();
    }
  },
  unmounted() {
    if (this.modelValue) {
      this.deactivate();
    }
  },
});
</script>