From ccbe88a5322d8fef057aa4610bf708b190656c4e Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Thu, 1 Oct 2020 21:41:39 +0200
Subject: [PATCH] #18 - Refine modal system

---
 resources/components/picker/stop.html  | 13 ++--
 resources/components/stop/details.html |  4 +-
 resources/components/ui/dialog.html    |  6 +-
 resources/styles/main.scss             |  4 ++
 resources/styles/ui/_modal.scss        | 15 +++-
 resources/ts/components/ui/dialog.ts   | 96 ++++++++++++++++++++++++--
 templates/app.html.twig                |  8 +--
 7 files changed, 127 insertions(+), 19 deletions(-)

diff --git a/resources/components/picker/stop.html b/resources/components/picker/stop.html
index b8d8997..63e3ebb 100644
--- a/resources/components/picker/stop.html
+++ b/resources/components/picker/stop.html
@@ -21,7 +21,7 @@
             <slot name="actions">
                 <button class="btn btn-action" ref="action-info" @click="details = !details">
                     <tooltip>dodatkowe informacje</tooltip>
-                    <ui-icon :icon="details ? 'info-hide' : 'info'"/>
+                    <ui-icon icon="info"/>
                 </button>
 
                 <button class="btn btn-action" ref="action-map" v-hover:map>
@@ -30,11 +30,16 @@
             </slot>
         </div>
     </div>
-    <fold :visible="details" class="stop__details-fold" lazy>
-        <stop-details :stop="stop"/>
-    </fold>
 
     <keep-alive>
+        <portal to="popups">
+            <ui-dialog v-if="details" @leave="details = false" behaviour="modal" class="ui-modal--medium" title="Szczegóły przystanku">
+                <stop-details :stop="stop"/>
+            </ui-dialog>
+        </portal>
+    </keep-alive>
+    <keep-alive>
+        <!-- FIXME: This should be in portal but it's not possible due to information loss, maybe in vue3 it will be better?-->
         <ui-dialog reference="action-map" v-if="showMap" arrow class="ui-popup--no-padding" style="width: 500px;" placement="right-start" v-hover:inMap>
             <stop-map :stop="stop" style="height: 300px"/>
         </ui-dialog>
diff --git a/resources/components/stop/details.html b/resources/components/stop/details.html
index e8d0927..f04dc1c 100644
--- a/resources/components/stop/details.html
+++ b/resources/components/stop/details.html
@@ -16,7 +16,9 @@
                 <div class="track__description">
                     {{ track.description }}
                 </div>
-                <span class="badge badge-pill badge-light track__order">#{{ order }}</span>
+                <span class="badge badge-pill badge-light track__order">
+                    #{{ order }}
+                </span>
             </li>
         </ul>
     </section>
diff --git a/resources/components/ui/dialog.html b/resources/components/ui/dialog.html
index 7adf4cb..8117b2e 100644
--- a/resources/components/ui/dialog.html
+++ b/resources/components/ui/dialog.html
@@ -1,9 +1,9 @@
 <div class="ui-backdrop" @click="handleBackdropClick" v-if="currentBehaviour === 'modal'">
-    <div class="ui-modal" v-bind="$attrs">
+    <div class="ui-modal" v-bind="attrs" v-on="$listeners">
         <div class="ui-modal__top-bar">
             <div class="ui-modal__header">
                 <slot name="header">
-                    <div class="ui-modal__title">{{ title }}</div>
+                    <div class="ui-modal__title"><slot name="title">{{ title }}</slot></div>
                 </slot>
             </div>
             <button class="btn btn-action ui-modal__close" @click.prevent="handleCloseClick">
@@ -16,7 +16,7 @@
         </div>
     </div>
 </div>
-<div :class="[ 'ui-popup', arrow && 'ui-popup--arrow' ]" v-bind="$attrs" v-on="$listeners" v-else>
+<div :class="[ 'ui-popup', arrow && 'ui-popup--arrow' ]" v-bind="attrs" :style="{ zIndex: zIndex }" v-on="$listeners" v-else>
     <div class="ui-popup__arrow" ref="arrow" v-if="arrow"></div>
     <div class="ui-popup__header" v-if="hasHeader">
         <slot name="header" />
diff --git a/resources/styles/main.scss b/resources/styles/main.scss
index 83d2198..63a5f4c 100644
--- a/resources/styles/main.scss
+++ b/resources/styles/main.scss
@@ -116,6 +116,10 @@ body {
   flex-direction: column;
   background: url("../images/background.png") repeat-x center bottom 63px;
 
+  &.contains-modal {
+    overflow-y: hidden;
+  }
+
   main {
     flex: 1 1 auto;
     position: relative;
diff --git a/resources/styles/ui/_modal.scss b/resources/styles/ui/_modal.scss
index 2764a52..2192306 100644
--- a/resources/styles/ui/_modal.scss
+++ b/resources/styles/ui/_modal.scss
@@ -7,11 +7,14 @@
   align-items: center;
   overflow-y: auto;
   overscroll-behavior-y: contain;
+  z-index: 10000;
 
   &::after {
-    height: 1rem;
+    height: $spacer;
     display: block;
     content: "";
+    width: 1px;
+    flex: 0 0 auto;
   }
 }
 
@@ -31,7 +34,7 @@ $dialog-sizes: (
   border-radius: 1px;
 
   @each $size, $width in $dialog-sizes {
-    .ui-modal--#{$size} {
+    &.ui-modal--#{$size} {
       width: $width;
     }
   }
@@ -56,3 +59,11 @@ $dialog-sizes: (
   display: flex;
   margin-bottom: $dialog-margin * 0.75;
 }
+
+@include media-breakpoint-down('sm') {
+  @each $size, $width in $dialog-sizes {
+    .ui-modal.ui-modal--#{$size} {
+      width: 100%;
+    }
+  }
+}
diff --git a/resources/ts/components/ui/dialog.ts b/resources/ts/components/ui/dialog.ts
index 4ecd057..18580ba 100644
--- a/resources/ts/components/ui/dialog.ts
+++ b/resources/ts/components/ui/dialog.ts
@@ -10,9 +10,31 @@ import { defaultBreakpoints } from "../../filters";
  */
 export type DialogBehaviour = "modal" | "popup";
 
+let openModalCounter: number = 0;
+
+function computeZIndexOfElement(element: HTMLElement): number {
+    let current = element;
+
+    while (true) {
+        const zIndex = window.getComputedStyle(current).zIndex;
+
+        if (zIndex !== "auto") {
+            return parseInt(zIndex);
+        }
+
+        if (!current.parentElement) {
+            break;
+        }
+
+        current = current.parentElement;
+    }
+
+    return 0;
+}
+
 @Component({
-    template: require('../../../components/ui/dialog.html'),
     inheritAttrs: false,
+    template: require('../../../components/ui/dialog.html'),
 })
 export default class UiDialog extends Vue {
     @Prop({ type: String, default: "popup" })
@@ -41,11 +63,23 @@ export default class UiDialog extends Vue {
 
     private isMobile: boolean = false;
 
+    /** Inherited class hack */
+    private staticClass: string[] = [];
+
+    private zIndex: number = 1000;
+
     private _focusOutEvent;
     private _resizeEvent;
 
     private _popper;
 
+    get attrs() {
+        return {
+            ...this.$attrs,
+            "class": this.staticClass
+        }
+    }
+
     get currentBehaviour(): DialogBehaviour {
         if (!this.mobileBehaviour) {
             return this.behaviour;
@@ -93,16 +127,60 @@ export default class UiDialog extends Vue {
     }
 
     mounted() {
+        this.zIndex = computeZIndexOfElement(this.getReferenceElement()) + 100;
+
         this.handleWindowResize();
 
         if (this.behaviour === 'popup') {
-            this.initPopper();
+            this.mountPopper();
         }
 
+        this.staticClass = Array.from(this.$el.classList).filter(cls => ["ui-backdrop", "ui-popup", "ui-popup--arrow"].indexOf(cls) === -1);
+
         window.addEventListener('resize', this._resizeEvent = this.handleWindowResize.bind(this));
+
+        this._activated();
     }
 
-    private initPopper() {
+    private _activated() {
+        if (this.behaviour === 'modal') {
+            this.mountModal();
+        }
+    }
+
+    private _deactivated() {
+        if (this.behaviour === 'modal') {
+            this.dismountModal();
+        }
+    }
+
+    private mountModal() {
+        if (openModalCounter === 0) {
+            document.body.style.paddingRight = `${window.screen.width - document.body.clientWidth}px`
+            document.body.classList.add('contains-modal');
+        }
+
+        openModalCounter++;
+    }
+
+    private dismountModal() {
+        openModalCounter--;
+
+        if (openModalCounter === 0) {
+            document.body.style.paddingRight = "";
+            document.body.classList.remove('contains-modal');
+        }
+    }
+
+    activated() {
+        this._activated();
+    }
+
+    deactivated() {
+        this._deactivated();
+    }
+
+    private mountPopper() {
         const reference = this.getReferenceElement();
 
         this._popper = new Popper(reference, this.$el, {
@@ -147,6 +225,8 @@ export default class UiDialog extends Vue {
 
     beforeDestroy() {
         this._focusOutEvent && document.removeEventListener('click', this._focusOutEvent, { capture: true });
+
+        this._deactivated()
     }
 
     removed() {
@@ -179,7 +259,15 @@ export default class UiDialog extends Vue {
         }
 
         if (newBehaviour === 'popup') {
-            this.$nextTick(() => this.initPopper());
+            this.$nextTick(() => this.mountPopper());
+        }
+
+        if (newBehaviour === 'modal') {
+            this.mountModal();
+        }
+
+        if (oldBehaviour === 'modal') {
+            this.dismountModal();
         }
     }
 }
diff --git a/templates/app.html.twig b/templates/app.html.twig
index 214d70c..90677c8 100644
--- a/templates/app.html.twig
+++ b/templates/app.html.twig
@@ -105,11 +105,9 @@
                         </button>
                     </div>
 
-                    <portal to="popups">
-                        <ui-dialog reference="settings-messages" v-if="visibility.messages" arrow placement="left-start" @leave="visibility.messages = false">
-                            <favourites-adder @saved="visibility.save = false"/>
-                        </ui-dialog>
-                    </portal>
+                    <ui-dialog reference="save" v-if="visibility.save" arrow placement="bottom-end" @leave="visibility.save = false">
+                        <favourites-adder @saved="visibility.save = false"/>
+                    </ui-dialog>
                 </section>
                 <section class="section picker">
                     <header class="section__title flex">