Tiptap RTE: Reduce loading layout shift (#19860)

* Tiptap RTE: Set row/group min-height to prevent layout shift

* Added `box-sizing: border-box`

* Adds loaded state to the editor

so that the border only appears once it's ready.

* Refactored toolbar to reduce the number of re-renders

* Refactored statusbar to reduce the number of re-renders
This commit is contained in:
Lee Kelleher
2025-08-08 07:41:44 +01:00
committed by GitHub
parent 5f1ecbae94
commit b3d5744d34
3 changed files with 81 additions and 41 deletions

View File

@@ -193,7 +193,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
return html`
${when(loading, () => html`<div id="loader"><uui-loader></uui-loader></div>`)}
${when(!loading, () => html`${this.#renderStyles()}${this.#renderToolbar()}`)}
<div id="editor" data-mark="input:tiptap-rte"></div>
<div id="editor" data-mark="input:tiptap-rte" ?data-loaded=${!loading}></div>
${when(!loading, () => this.#renderStatusbar())}
`;
}
@@ -277,7 +277,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
display: flex;
overflow: auto;
border-radius: var(--uui-border-radius);
border: 1px solid var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border: 1px solid transparent;
padding: 1rem;
box-sizing: border-box;
@@ -288,6 +288,10 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
width: 100%;
max-width: 100%;
&[data-loaded] {
border-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
}
> .tiptap {
height: 100%;
width: 100%;

View File

@@ -1,18 +1,26 @@
import type { UmbTiptapStatusbarValue } from '../types.js';
import { css, customElement, html, map, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, nothing, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { debounce } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
/**
* Provides a status bar for the {@link UmbInputTiptapElement}
* @element umb-tiptap-statusbar
* @cssprop --umb-tiptap-edge-border-color - Defines the edge border color
*/
@customElement('umb-tiptap-statusbar')
export class UmbTiptapStatusbarElement extends UmbLitElement {
#attached = false;
#debouncer = debounce(() => this.requestUpdate(), 100);
#extensionsController?: UmbExtensionsElementInitializer;
@state()
private _lookup?: Map<string, unknown>;
#lookup: Map<string, unknown> = new Map();
@property({ type: Boolean, reflect: true })
readonly = false;
@@ -61,12 +69,13 @@ export class UmbTiptapStatusbarElement extends UmbLitElement {
'tiptapStatusbarExtension',
(manifest) => this.statusbar.flat().includes(manifest.alias),
(extensionControllers) => {
this._lookup = new Map(
extensionControllers.map((ext) => {
extensionControllers.forEach((ext) => {
if (!this.#lookup.has(ext.alias)) {
(ext.component as HTMLElement)?.setAttribute('data-mark', `action:tiptap-statusbar:${ext.alias}`);
return [ext.alias, ext.component];
}),
);
this.#lookup.set(ext.alias, ext.component);
this.#debouncer();
}
});
},
);
@@ -75,10 +84,15 @@ export class UmbTiptapStatusbarElement extends UmbLitElement {
override render() {
if (!this.statusbar.flat().length) return nothing;
return map(
this.statusbar,
(area) => html`<div class="area">${map(area, (alias) => this._lookup?.get(alias) ?? nothing)}</div>`,
);
return this.#renderAreas(this.statusbar);
}
#renderAreas(statusbar: UmbTiptapStatusbarValue) {
return repeat(statusbar, (area) => html`<div class="area">${this.#renderActions(area)}</div>`);
}
#renderActions(aliases: Array<string>) {
return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? nothing);
}
static override readonly styles = css`
@@ -95,10 +109,11 @@ export class UmbTiptapStatusbarElement extends UmbLitElement {
justify-content: space-between;
border-radius: var(--uui-border-radius);
border: 1px solid var(--uui-color-border);
border: 1px solid var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top: 0;
box-sizing: border-box;
min-height: var(--uui-size-layout-1);
max-height: var(--uui-size-layout-2);

View File

@@ -1,5 +1,6 @@
import type { UmbTiptapToolbarValue } from '../types.js';
import { css, customElement, html, map, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, nothing, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { debounce } from '@umbraco-cms/backoffice/utils';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -9,18 +10,20 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/
import '../cascading-menu-popover/cascading-menu-popover.element.js';
/**
* Provides a sticky toolbar for the {@link UmbInputTiptapElement}
* @element umb-tiptap-toolbar
* @cssprop --umb-tiptap-edge-border-color - Defines the edge border color
* @cssprop --umb-tiptap-top - Defines the top value for the sticky toolbar
*/
* Provides a sticky toolbar for the {@link UmbInputTiptapElement}
* @element umb-tiptap-toolbar
* @cssprop --umb-tiptap-edge-border-color - Defines the edge border color
* @cssprop --umb-tiptap-top - Defines the top value for the sticky toolbar
*/
@customElement('umb-tiptap-toolbar')
export class UmbTiptapToolbarElement extends UmbLitElement {
#attached = false;
#debouncer = debounce(() => this.requestUpdate(), 100);
#extensionsController?: UmbExtensionsElementAndApiInitializer;
@state()
private _lookup?: Map<string, unknown>;
#lookup: Map<string, unknown> = new Map();
@property({ type: Boolean, reflect: true })
readonly = false;
@@ -58,12 +61,13 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
[],
(manifest) => this.toolbar.flat(2).includes(manifest.alias),
(extensionControllers) => {
this._lookup = new Map(
extensionControllers.map((ext) => {
extensionControllers.forEach((ext) => {
if (!this.#lookup.has(ext.alias)) {
(ext.component as HTMLElement)?.setAttribute('data-mark', `action:tiptap-toolbar:${ext.alias}`);
return [ext.alias, ext.component];
}),
);
this.#lookup.set(ext.alias, ext.component);
this.#debouncer();
}
});
},
undefined,
undefined,
@@ -76,18 +80,23 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
override render() {
if (!this.toolbar.flat(2).length) return nothing;
return this.#renderRows(this.toolbar);
}
return map(
this.toolbar,
(row) => html`
<div class="row">
${map(
row,
(group) => html`<div class="group">${map(group, (alias) => this._lookup?.get(alias) ?? nothing)}</div>`,
)}
</div>
`,
);
#renderRows(rows: UmbTiptapToolbarValue) {
return repeat(rows, (row) => html`<div class="row">${this.#renderGroups(row)}</div>`);
}
#renderGroups(groups: Array<Array<string>>) {
return repeat(groups, (group) => html`<div class="group">${this.#renderActions(group)}</div>`);
}
#renderActions(aliases: Array<string>) {
return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder());
}
#renderActionPlaceholder() {
return html`<span class="skeleton" role="none"></span>`;
}
static override readonly styles = css`
@@ -105,6 +114,7 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
border-top-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border-left-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
border-right-color: var(--umb-tiptap-edge-border-color, var(--uui-color-border));
box-sizing: border-box;
background-color: var(--uui-color-surface);
color: var(--color-text);
@@ -114,7 +124,7 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
flex-direction: column;
position: sticky;
top: var(--umb-tiptap-top,-25px);
top: var(--umb-tiptap-top, -25px);
left: 0;
right: 0;
padding: var(--uui-size-3);
@@ -130,21 +140,32 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
flex-direction: row;
flex-wrap: wrap;
min-height: var(--uui-size-12, 36px);
.group {
display: inline-flex;
flex-wrap: wrap;
align-items: stretch;
min-height: var(--uui-size-12, 36px);
&:not(:last-child)::after {
content: '';
background-color: var(--uui-color-border);
width: 1px;
place-self: center;
height: 22px;
height: var(--uui-size-7, 21px);
margin: 0 var(--uui-size-3);
}
}
}
.skeleton {
background-color: var(--uui-color-background);
height: var(--uui-size-12, 36px);
width: var(--uui-size-10, 30px);
margin-left: 1px;
}
`;
}