Skip to content

Commit

Permalink
Add support for teleporting
Browse files Browse the repository at this point in the history
that is performant and simple to use.

- The teleport root element is inserted to an application's
  document body during KDS initialization
  - Removes the need for manual insert
  - Ensures we never attach teleported elements, such as tooltips,
    to document.body itself, which is said to cause severe performance
    problems (https://css-tricks.com/dont-attach-tooltips-to-document-body/)
- Adds new `KTeleport` component
  - Wrapper around vue2-teleport with restricted API that only allows
    teleporting to the KDS teleport root element.
- Removes Teleport in favour of KTeleport in KModal
- Adds appendToRoot prop to KTooltip
  - In support of Studio migration to KDS where in a few instances,
    teleport is needed for tooltips to display correctly
  - Contains smaller refactor of Popper.vue to allow the tooltip
    be attached to an element different from document.body
  • Loading branch information
MisRob committed Aug 11, 2024
1 parent 41d4813 commit 52656ca
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 22 deletions.
3 changes: 2 additions & 1 deletion docs/pages/installation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
<li>Installs <code>$themeBrand</code>, <code>$themeTokens</code> <code>$themePalette</code>, and <code>$computedClass</code> helpers on all Vue instances (see <DocsInternalLink href="/colors/#usage" text="Colors" />).</li>
<li>Provides <code>$coreOutline</code>, <code>$inputModality</code>, <code>$mediaType</code>, and <code>$isPrint</code> computed properties as well as <code>$print</code> method to all Vue instances.</li>
<li>Globally registers all KDS Vue components.</li>
<li>Inserts assertive and polite ARIA live regions to your application's document body (see <DocsInternalLink href="/usekliveregion" text="useKLiveRegion" />).</li>
<li>Inserts assertive and polite ARIA live regions <code>#k-live-region</code> to your application's document body (see <DocsInternalLink href="/usekliveregion" text="useKLiveRegion" />).</li>
<li>Inserts the teleport root element <code>#k-teleport</code> to your application's document body (see <DocsLibraryLink component="KTeleport" /> or search for <code>appendToRoot</code> prop on components).</li>
</ul>
</DocsPageSection>

Expand Down
29 changes: 29 additions & 0 deletions docs/pages/kteleport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>

<DocsPageTemplate apiDocs>
<DocsPageSection title="Overview" anchor="#overview">
<p>Wrapping a component in <code>KTeleport</code> will teleport it to the teleport root element that is inserted to an application's document body automatically <DocsInternalLink href="/installation#install-plugin" text="during the KDS installation process" />.</p>
</DocsPageSection>

<DocsPageSection title="Usage" anchor="#usage">
<p>First, check whether you need <code>KTeleport</code>. Some KDS components already utilize it internally and you only need to instruct them to activate it, typically via the <code>appendToRoot</code> prop.</p>

<p>If you need to use <code>KTeleport</code>, simply wrap your component in it:</p>

<DocsShowCode language="html">
<KTeleport>
<YourComponent />
</KTeleport>
</DocsShowCode>
</DocsPageSection>

<DocsPageSection title="Related" anchor="#related">
<ul>
<li>
<DocsInternalLink href="/installation#install-plugin" text="KDS installation step" /> that attaches the teleport root element to an application's document body
</li>
</ul>
</DocsPageSection>
</DocsPageTemplate>

</template>
2 changes: 1 addition & 1 deletion docs/pages/usekliveregion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<DocsPageSection title="Related" anchor="#related">
<ul>
<li>
<DocsInternalLink href="/installation#install-plugin" text="KDS installation step" /> that attaches live regions to an application's DOM
<DocsInternalLink href="/installation#install-plugin" text="KDS installation step" /> that attaches live regions to an application's document body
</li>
</ul>
</DocsPageSection>
Expand Down
5 changes: 5 additions & 0 deletions docs/tableOfContents.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ export default [
isCode: true,
keywords: tabsRelatedKeywords,
}),
new Page({
path: '/kteleport',
title: 'KTeleport',
isCode: true,
}),
new Page({
path: '/ktransition',
title: 'KTransition',
Expand Down
11 changes: 2 additions & 9 deletions lib/KModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>

<component :is="wrapper" v-bind="wrapperProps">
<component :is="wrapper">
<!-- Accessibility properties for the overlay -->
<transition name="modal-fade" appear>
<div
Expand Down Expand Up @@ -103,7 +103,6 @@

<script>
import Teleport from 'vue2-teleport';
import debounce from 'lodash/debounce';
import useKResponsiveWindow from './composables/useKResponsiveWindow';
Expand All @@ -120,9 +119,6 @@
*/
export default {
name: 'KModal',
components: {
Teleport,
},
setup() {
const { windowHeight, windowWidth } = useKResponsiveWindow();
return { windowHeight, windowWidth };
Expand Down Expand Up @@ -236,10 +232,7 @@
};
},
wrapper() {
return this.appendToRoot ? 'Teleport' : 'div';
},
wrapperProps() {
return this.appendToRoot ? { to: 'body' } : {};
return this.appendToRoot ? 'KTeleport' : 'div';
},
},
created() {
Expand Down
34 changes: 34 additions & 0 deletions lib/KTeleport/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>

<Teleport :to="teleportRootElSelector">
<!-- @slot Content to be teleported -->
<slot></slot>
</Teleport>

</template>


<script>
import Teleport from 'vue2-teleport';
import _useTeleportRootEl from '../composables/_useTeleportRootEl';
/**
* Wrapping a component in KTeleport will teleport it to
* the teleport root element #k-teleport that is inserted
* to an application's document body automatically during
* the KDS installation process (see KThemePlugin.js).
*/
export default {
name: 'KTeleport',
components: {
Teleport,
},
setup() {
const { teleportRootElSelector } = _useTeleportRootEl();
return { teleportRootElSelector };
},
};
</script>

5 changes: 5 additions & 0 deletions lib/KThemePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import KSwitch from './KSwitch';
import KTabs from './tabs/KTabs';
import KTabsList from './tabs/KTabsList';
import KTabsPanel from './tabs/KTabsPanel';
import KTeleport from './KTeleport';
import KTextbox from './KTextbox';
import KTooltip from './KTooltip';
import KTransition from './KTransition';
Expand All @@ -41,8 +42,10 @@ import { themeTokens, themeBrand, themePalette, themeOutlineStyle } from './styl
import globalThemeState from './styles/globalThemeState';

import useKLiveRegion from './composables/useKLiveRegion';
import _useTeleportRootEl from './composables/_useTeleportRootEl';

const { _mountLiveRegion } = useKLiveRegion();
const { mountTeleportRootEl } = _useTeleportRootEl();

require('./grids/globalStyles.js'); // global grid styles

Expand All @@ -54,6 +57,7 @@ export default function KThemePlugin(Vue) {
if (!isNuxtServerSideRendering()) {
const onDomReady = () => {
_mountLiveRegion();
mountTeleportRootEl();
document.removeEventListener('DOMContentLoaded', onDomReady);
};

Expand Down Expand Up @@ -142,6 +146,7 @@ export default function KThemePlugin(Vue) {
Vue.component('KTabs', KTabs);
Vue.component('KTabsList', KTabsList);
Vue.component('KTabsPanel', KTabsPanel);
Vue.component('KTeleport', KTeleport);
Vue.component('KTextbox', KTextbox);
Vue.component('KTooltip', KTooltip);
Vue.component('KTransition', KTransition);
Expand Down
26 changes: 16 additions & 10 deletions lib/KTooltip/Popper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
Vendored from https://github.com/RobinCK/vue-popper/
pending
https://github.com/RobinCK/vue-popper/pull/73
LE customizations
- Allow for appending to a chosen element rather than to body,
typically to the teleport root element. 'appendToBody' prop changed
to 'appendToEl' and related changes.
-->


Expand Down Expand Up @@ -89,9 +94,10 @@
default: false,
},
dataValue: { default: null }, // eslint-disable-line
appendToBody: {
type: Boolean,
default: false,
/* An HTML element the tooltip should be appended to */
appendToEl: {
type: Object,
default: null,
},
visibleArrow: {
type: Boolean,
Expand Down Expand Up @@ -155,7 +161,7 @@
created() {
this.appendedArrow = false;
this.appendedToBody = false;
this.isAppendedToEl = false;
this.popperOptions = Object.assign(this.popperOptions, this.options);
},
Expand Down Expand Up @@ -210,9 +216,9 @@
this.popperJS = null;
}
if (this.appendedToBody) {
this.appendedToBody = false;
document.body.removeChild(this.popper.parentElement);
if (this.isAppendedToEl) {
this.isAppendedToEl = false;
this.appendToEl.removeChild(this.popper.parentElement);
}
},
Expand All @@ -222,9 +228,9 @@
this.appendArrow(this.popper);
}
if (this.appendToBody && !this.appendedToBody) {
this.appendedToBody = true;
document.body.appendChild(this.popper.parentElement);
if (this.appendToEl && !this.isAppendedToEl) {
this.isAppendedToEl = true;
this.appendToEl.appendChild(this.popper.parentElement);
}
if (this.popperJS && this.popperJS.destroy) {
Expand Down
16 changes: 16 additions & 0 deletions lib/KTooltip/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
:disabled="disabled"
:visibleArrow="false"
:options="options"
:appendToEl="appendToEl"
trigger="hover"
>
<div
Expand All @@ -28,6 +29,7 @@
<script>
import isArray from 'lodash/isArray';
import _useTeleportRootEl from '../composables/_useTeleportRootEl';
import Popper from './Popper';
/**
Expand All @@ -38,6 +40,11 @@
components: {
Popper,
},
setup(props) {
const { getTeleportRootEl } = _useTeleportRootEl();
const appendToEl = props.appendToRoot ? getTeleportRootEl() : null;
return { appendToEl };
},
props: {
/**
* Name of `ref` element within the parent's `$refs` object. Tooltip will be
Expand Down Expand Up @@ -86,6 +93,15 @@
type: String,
default: null,
},
/**
* Whether or not the tooltip should be teleported
* to the root of the document
*/
// eslint-disable-next-line kolibri/vue-no-unused-properties
appendToRoot: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down
10 changes: 10 additions & 0 deletions lib/composables/_useTeleportRootEl/__tests__/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe('_useTeleportRootEl', () => {
// this is taken care of by KThemePlugin.js that is already registered
// in the global jest setup
it(`document body contains k-teleport root element upon the KDS plugin initialization`, () => {
expect(`
<div id="k-teleport">
</div>
`).toBeInDom();
});
});
59 changes: 59 additions & 0 deletions lib/composables/_useTeleportRootEl/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const TELEPORT_ROOT_EL_ID = 'k-teleport';

/**
* A private composable that takes care of everything
* related to the teleport root element that is inserted
* to an application's document body during the KDS
* initialization (see KThemePlugin.js)
*/
export default function _useTeleportRootEl() {
const teleportRootElSelector = `#${TELEPORT_ROOT_EL_ID}`;

/**
* Inserts the teleport root element to an application's
* document body and saves the element to window for later use.
* Should be called only once, typically during app
* initialization.
*/
function mountTeleportRootEl() {
const teleportRootEl = document.getElementById(TELEPORT_ROOT_EL_ID);

// Already mounted, so don't mount again,
// just make sure elements are available in window
if (teleportRootEl) {
window.teleportRootEl = teleportRootEl;
return;
}

const newTeleportRootEl = document.createElement('div');
newTeleportRootEl.id = TELEPORT_ROOT_EL_ID;

document.body.insertBefore(newTeleportRootEl, document.body.firstChild);

// Save for later use so that we don't need to query
// every time we call the 'getTeleportRootEl'
// (frequent operation due to the usage from
// KTooltip and other components).
window.teleportRootEl = newTeleportRootEl;
}

/**
* @returns {HTMLElement} The teleport root element
*/
function getTeleportRootEl() {
// do not query DOM for performance reasons
const teleportRootEl = window.teleportRootEl;
// unlikely to happen, but just in case
if (!teleportRootEl) {
console.error('[KDS] The teleport root element is missing. KDS initialization failed?');
return;
}
return teleportRootEl;
}

return {
mountTeleportRootEl,
getTeleportRootEl,
teleportRootElSelector,
};
}
2 changes: 1 addition & 1 deletion lib/composables/useKLiveRegion/__tests__/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import useKLiveRegion from '../index.js';
const { sendPoliteMessage, sendAssertiveMessage } = useKLiveRegion();

describe('useKLiveRegion', () => {
// this is take care of by KThemePlugin.js that is already registered
// this is taken care of by KThemePlugin.js that is already registered
// in the global jest setup
it(`document body contains live regions upon the KDS plugin initialization`, () => {
expect(`
Expand Down

0 comments on commit 52656ca

Please sign in to comment.