Files
hoppscotch/packages/hoppscotch-ui/src/components/smart/Modal.vue
Anwarul Islam 0fcda0be1a refactor: hoppscotch ui (#2887)
* feat: hopp ui initialized

* feat: button components added

* feat: windi css integration

* chore: package removed from hopp ui

* feat: storybook added

* feat: move all smart components hoppscotch-ui

* fix: import issue from components/smart

* fix: env input component import

* feat: add hoppui to windicss config

* fix: remove storybook

* feat: move components from hoppscotch-ui

* feat: storybook added

* feat: storybook progress

* feat: themeing storybook

* feat: add stories

* chore: package updated

* chore: stories added

* feat: stories added

* feat: stories added

* feat: icons resolved

* feat: i18n composable resolved

* feat: histoire added

* chore: resolved prettier issue

* feat: radio story added

* feat: story added for all components

* feat: new components added to stories

* fix: resolved issues

* feat: readme.md added

* feat: context/provider added

* chore: removed app component registry

* chore: remove importing of all components in hopp-ui to allow code splitting

* chore: fix vite config errors

* chore: jsdoc added

* chore: any replaced with smart-item

* chore: i18n added to ui components

* chore: clean up - removed a duplicate button

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-01-28 08:27:00 +05:30

220 lines
5.1 KiB
Vue

<template>
<Transition name="fade" appear @leave="onTransitionLeaveStart">
<div
ref="modal"
class="fixed inset-0 z-50 overflow-y-auto transition"
role="dialog"
>
<div
class="flex items-end justify-center min-h-screen text-center sm:block"
>
<Transition name="fade" appear>
<div
class="fixed inset-0 transition-opacity"
@touchstart="!dialog ? close() : null"
@touchend="!dialog ? close() : null"
@mouseup="!dialog ? close() : null"
@mousedown="!dialog ? close() : null"
>
<div
class="absolute inset-0 opacity-80 bg-primaryLight focus:outline-none"
tabindex="0"
@click="!dialog ? close() : null"
></div>
</div>
</Transition>
<span
v-if="placement === 'center'"
class="sm:h-screen <sm:hidden sm:align-middle"
aria-hidden="true"
>&#8203;</span
>
<Transition name="bounce" appear>
<div
class="inline-block w-full overflow-hidden text-left align-bottom transition-all transform shadow-lg sm:border border-dividerDark bg-primary sm:rounded-xl sm:align-middle"
:class="[{ 'mt-24 md:mb-8': placement === 'top' }, styles]"
>
<div
v-if="title"
class="flex items-center justify-between border-b border-dividerLight"
:class="{ 'p-4': !fullWidth }"
>
<h3 class="heading" :class="{ 'ml-4': !fullWidth }">
{{ title }}
</h3>
<span class="flex items-center">
<slot name="actions"></slot>
<kbd class="mr-2 shortcut-key">ESC</kbd>
<ButtonSecondary
v-if="dimissible"
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="closeText ?? t?.('action.close') ?? 'Close'"
:icon="IconX"
@click="close"
/>
</span>
</div>
<div
class="flex flex-col overflow-y-auto max-h-lg"
:class="{ 'p-4': !fullWidth }"
>
<slot name="body"></slot>
</div>
<div
v-if="hasFooterSlot"
class="flex items-center justify-between flex-1 border-t border-dividerLight bg-primaryContrast"
:class="{ 'p-4': !fullWidth }"
>
<slot name="footer"></slot>
</div>
</div>
</Transition>
</div>
</div>
</Transition>
</template>
<script lang="ts">
const PORTAL_DOM_ID = "hoppscotch-modal-portal"
// An ID ticker for generating consistently unique modal IDs
let stackIDTicker = 0
// Why ?
const stack = (() => {
const stack: number[] = []
return {
push: stack.push.bind(stack),
pop: stack.pop.bind(stack),
peek: () => (stack.length === 0 ? undefined : stack[stack.length - 1]),
}
})()
</script>
<script setup lang="ts">
import IconX from "~icons/lucide/x"
import {
ref,
computed,
useSlots,
onMounted,
onBeforeUnmount,
inject,
} from "vue"
import { HoppUIPluginOptions, HOPP_UI_OPTIONS } from "./../../index"
const { t, onModalOpen, onModalClose } =
inject<HoppUIPluginOptions>(HOPP_UI_OPTIONS) ?? {}
defineProps({
dialog: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "",
},
dimissible: {
type: Boolean,
default: true,
},
placement: {
type: String,
default: "top",
},
fullWidth: {
type: Boolean,
default: false,
},
styles: {
type: String,
default: "sm:max-w-lg",
},
closeText: {
type: String,
default: null,
},
})
const emit = defineEmits<{
(e: "close"): void
}>()
onBeforeUnmount(() => {
onModalClose?.()
})
const stackId = ref(stackIDTicker++)
const shouldCleanupDomOnUnmount = ref(true)
const slots = useSlots()
const hasFooterSlot = computed(() => {
return !!slots.footer
})
const modal = ref<Element>()
onMounted(() => {
const portal = getPortal()
portal.appendChild(modal.value!)
stack.push(stackId.value)
document.addEventListener("keydown", onKeyDown)
onModalOpen?.()
})
onBeforeUnmount(() => {
if (shouldCleanupDomOnUnmount.value && modal.value) {
getPortal().removeChild(modal.value)
}
stack.pop()
document.removeEventListener("keydown", onKeyDown)
})
const close = () => {
emit("close")
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && stackId.value === stack.peek()) {
e.preventDefault()
close()
}
}
const onTransitionLeaveStart = () => {
close()
shouldCleanupDomOnUnmount.value = false
}
const getPortal = () => {
let el = document.querySelector("#" + PORTAL_DOM_ID)
if (el) {
return el
}
el = document.createElement("DIV")
el.id = PORTAL_DOM_ID
document.body.appendChild(el)
return el
}
</script>
<style lang="scss" scoped>
.bounce-enter-active {
@apply transition;
animation: bounce-in 150ms;
}
@keyframes bounce-in {
0% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}
</style>