From b3d5744d34e7a8182f3aa20bda235c9e43c6ef57 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Fri, 8 Aug 2025 07:41:44 +0100 Subject: [PATCH] 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 --- .../input-tiptap/input-tiptap.element.ts | 8 +- .../input-tiptap/tiptap-statusbar.element.ts | 41 +++++++---- .../input-tiptap/tiptap-toolbar.element.ts | 73 ++++++++++++------- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index f575f25b0b..0bb9fdbc66 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -193,7 +193,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin html`
`)} ${when(!loading, () => html`${this.#renderStyles()}${this.#renderToolbar()}`)} -
+
${when(!loading, () => this.#renderStatusbar())} `; } @@ -277,7 +277,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin .tiptap { height: 100%; width: 100%; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-statusbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-statusbar.element.ts index 8e00b6ee73..28293caa37 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-statusbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-statusbar.element.ts @@ -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; + #lookup: Map = 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`
${map(area, (alias) => this._lookup?.get(alias) ?? nothing)}
`, - ); + return this.#renderAreas(this.statusbar); + } + + #renderAreas(statusbar: UmbTiptapStatusbarValue) { + return repeat(statusbar, (area) => html`
${this.#renderActions(area)}
`); + } + + #renderActions(aliases: Array) { + 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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index 78aeb69f79..3ac467ad43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -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; + #lookup: Map = 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` -
- ${map( - row, - (group) => html`
${map(group, (alias) => this._lookup?.get(alias) ?? nothing)}
`, - )} -
- `, - ); + #renderRows(rows: UmbTiptapToolbarValue) { + return repeat(rows, (row) => html`
${this.#renderGroups(row)}
`); + } + + #renderGroups(groups: Array>) { + return repeat(groups, (group) => html`
${this.#renderActions(group)}
`); + } + + #renderActions(aliases: Array) { + return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder()); + } + + #renderActionPlaceholder() { + return html``; } 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; + } `; }