mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	 696611ae63
			
		
	
	696611ae63
	
	
	
		
			
			# Pull Request Template ## Description This PR introduces a `CustomTeleport` component that wraps Vue's Teleport to preserve directionality context (ltr / rtl) when teleporting content outside the app’s root container. ### Problem Currently, the app sets the text direction (`[dir="ltr"]` or `[dir="rtl"]`) on a container `div` inside `<body>`, not on `<body>` itself. When content is teleported directly into `body`, it no longer inherits the correct direction context. As a result, direction-aware utility classes like `ltr:pl-2`, `rtl:ml-1`, etc., break because CSS selectors like `[dir="ltr"] .ltr\:pl-2` no longer match. Identified this issue when working on this [PR](https://github.com/chatwoot/chatwoot/pull/11382) ### Solution The `CustomTeleport` component automatically applies the correct `[dir]` attribute (`ltr` or `rtl`) on the teleported content's wrapper based on the current `isRTL` setting from the store. This ensures that direction-specific Tailwind utility classes continue to work as expected, even when the content is rendered outside the app root. ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranavrajs@gmail.com>
		
			
				
	
	
		
			160 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			160 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
| <script setup>
 | |
| import { ref, computed } from 'vue';
 | |
| import { OnClickOutside } from '@vueuse/components';
 | |
| import { useI18n } from 'vue-i18n';
 | |
| 
 | |
| import Button from 'dashboard/components-next/button/Button.vue';
 | |
| import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
 | |
| 
 | |
| const props = defineProps({
 | |
|   type: {
 | |
|     type: String,
 | |
|     default: 'edit',
 | |
|     validator: value => ['alert', 'edit'].includes(value),
 | |
|   },
 | |
|   title: {
 | |
|     type: String,
 | |
|     default: '',
 | |
|   },
 | |
|   description: {
 | |
|     type: String,
 | |
|     default: '',
 | |
|   },
 | |
|   cancelButtonLabel: {
 | |
|     type: String,
 | |
|     default: '',
 | |
|   },
 | |
|   confirmButtonLabel: {
 | |
|     type: String,
 | |
|     default: '',
 | |
|   },
 | |
|   disableConfirmButton: {
 | |
|     type: Boolean,
 | |
|     default: false,
 | |
|   },
 | |
|   isLoading: {
 | |
|     type: Boolean,
 | |
|     default: false,
 | |
|   },
 | |
|   showCancelButton: {
 | |
|     type: Boolean,
 | |
|     default: true,
 | |
|   },
 | |
|   showConfirmButton: {
 | |
|     type: Boolean,
 | |
|     default: true,
 | |
|   },
 | |
|   overflowYAuto: {
 | |
|     type: Boolean,
 | |
|     default: false,
 | |
|   },
 | |
|   width: {
 | |
|     type: String,
 | |
|     default: 'lg',
 | |
|     validator: value => ['3xl', '2xl', 'xl', 'lg', 'md', 'sm'].includes(value),
 | |
|   },
 | |
| });
 | |
| 
 | |
| const emit = defineEmits(['confirm', 'close']);
 | |
| 
 | |
| const { t } = useI18n();
 | |
| 
 | |
| const dialogRef = ref(null);
 | |
| const dialogContentRef = ref(null);
 | |
| 
 | |
| const maxWidthClass = computed(() => {
 | |
|   const classesMap = {
 | |
|     '3xl': 'max-w-3xl',
 | |
|     '2xl': 'max-w-2xl',
 | |
|     xl: 'max-w-xl',
 | |
|     lg: 'max-w-lg',
 | |
|     md: 'max-w-md',
 | |
|     sm: 'max-w-sm',
 | |
|   };
 | |
| 
 | |
|   return classesMap[props.width] ?? 'max-w-md';
 | |
| });
 | |
| 
 | |
| const open = () => {
 | |
|   dialogRef.value?.showModal();
 | |
| };
 | |
| 
 | |
| const close = () => {
 | |
|   emit('close');
 | |
|   dialogRef.value?.close();
 | |
| };
 | |
| 
 | |
| const confirm = () => {
 | |
|   emit('confirm');
 | |
| };
 | |
| 
 | |
| defineExpose({ open, close });
 | |
| </script>
 | |
| 
 | |
| <template>
 | |
|   <TeleportWithDirection to="body">
 | |
|     <dialog
 | |
|       ref="dialogRef"
 | |
|       class="w-full transition-all duration-300 ease-in-out shadow-xl rounded-xl"
 | |
|       :class="[
 | |
|         maxWidthClass,
 | |
|         overflowYAuto ? 'overflow-y-auto' : 'overflow-visible',
 | |
|       ]"
 | |
|       @close="close"
 | |
|     >
 | |
|       <OnClickOutside @trigger="close">
 | |
|         <form
 | |
|           ref="dialogContentRef"
 | |
|           class="flex flex-col w-full h-auto gap-6 p-6 overflow-visible text-left align-middle transition-all duration-300 ease-in-out transform bg-n-alpha-3 backdrop-blur-[100px] shadow-xl rounded-xl"
 | |
|           @submit.prevent="confirm"
 | |
|           @click.stop
 | |
|         >
 | |
|           <div v-if="title || description" class="flex flex-col gap-2">
 | |
|             <h3 class="text-base font-medium leading-6 text-n-slate-12">
 | |
|               {{ title }}
 | |
|             </h3>
 | |
|             <slot name="description">
 | |
|               <p v-if="description" class="mb-0 text-sm text-n-slate-11">
 | |
|                 {{ description }}
 | |
|               </p>
 | |
|             </slot>
 | |
|           </div>
 | |
|           <slot />
 | |
|           <!-- Dialog content will be injected here -->
 | |
|           <slot name="footer">
 | |
|             <div
 | |
|               v-if="showCancelButton || showConfirmButton"
 | |
|               class="flex items-center justify-between w-full gap-3"
 | |
|             >
 | |
|               <Button
 | |
|                 v-if="showCancelButton"
 | |
|                 variant="faded"
 | |
|                 color="slate"
 | |
|                 :label="cancelButtonLabel || t('DIALOG.BUTTONS.CANCEL')"
 | |
|                 class="w-full"
 | |
|                 type="button"
 | |
|                 @click="close"
 | |
|               />
 | |
|               <Button
 | |
|                 v-if="showConfirmButton"
 | |
|                 :color="type === 'edit' ? 'blue' : 'ruby'"
 | |
|                 :label="confirmButtonLabel || t('DIALOG.BUTTONS.CONFIRM')"
 | |
|                 class="w-full"
 | |
|                 :is-loading="isLoading"
 | |
|                 :disabled="disableConfirmButton || isLoading"
 | |
|                 type="submit"
 | |
|               />
 | |
|             </div>
 | |
|           </slot>
 | |
|         </form>
 | |
|       </OnClickOutside>
 | |
|     </dialog>
 | |
|   </TeleportWithDirection>
 | |
| </template>
 | |
| 
 | |
| <style scoped>
 | |
| dialog::backdrop {
 | |
|   @apply bg-n-alpha-black1 backdrop-blur-[4px];
 | |
| }
 | |
| </style>
 |