Files
chatwoot/app/javascript/dashboard/components/ui/ContextMenu.vue
Sivin Varghese 696611ae63 feat: Add Teleport component to fix RTL/LTR utility classes (#11455)
# 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>
2025-05-12 11:49:23 -07:00

74 lines
1.7 KiB
Vue

<script setup>
import { computed, onMounted, nextTick, useTemplateRef } from 'vue';
import { useWindowSize, useElementBounding } from '@vueuse/core';
import TeleportWithDirection from 'dashboard/components-next/TeleportWithDirection.vue';
const props = defineProps({
x: { type: Number, default: 0 },
y: { type: Number, default: 0 },
});
const emit = defineEmits(['close']);
const menuRef = useTemplateRef('menuRef');
const { width: windowWidth, height: windowHeight } = useWindowSize();
const { width: menuWidth, height: menuHeight } = useElementBounding(menuRef);
const calculatePosition = (x, y, menuW, menuH, windowW, windowH) => {
// Initial position
let left = x;
let top = y;
// Boundary checks
const isOverflowingRight = left + menuW > windowW;
const isOverflowingBottom = top + menuH > windowH;
// Adjust position if overflowing
if (isOverflowingRight) left = windowW - menuW;
if (isOverflowingBottom) top = windowH - menuH;
return {
left: Math.max(0, left),
top: Math.max(0, top),
};
};
const position = computed(() => {
if (!menuRef.value) return { top: `${props.y}px`, left: `${props.x}px` };
const { left, top } = calculatePosition(
props.x,
props.y,
menuWidth.value,
menuHeight.value,
windowWidth.value,
windowHeight.value
);
return {
top: `${top}px`,
left: `${left}px`,
};
});
onMounted(() => {
nextTick(() => menuRef.value?.focus());
});
</script>
<template>
<TeleportWithDirection to="body">
<div
ref="menuRef"
class="fixed outline-none z-[9999] cursor-pointer"
:style="position"
tabindex="0"
@blur="emit('close')"
>
<slot />
</div>
</TeleportWithDirection>
</template>