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">