From 09f76df8890c55094d0b7773250baaaefa5e1e7c Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 12 May 2026 14:05:21 +0100 Subject: [PATCH 1/4] feat(components): add umb-entity-frame component --- .../entity-frame/entity-frame.element.ts | 83 +++++++++++++++++++ .../entity-frame/entity-frame.test.ts | 42 ++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts new file mode 100644 index 000000000000..25f112835023 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts @@ -0,0 +1,83 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, property, LitElement } from '@umbraco-cms/backoffice/external/lit'; + +/** + * A passive overlay that frames its parent with a rounded border and shows a label tab + * just above the parent's top-right corner. Visibility is controlled by the consumer via + * `--umb-entity-frame-opacity` (defaults to `1`); the typical pattern is for the parent + * container to set it to `0` by default and toggle to `1` on `:hover` and/or `:focus-within`. + * The parent must establish a positioning context (e.g. `position: relative`), and must not + * clip overflow above its top edge (the tab renders outside the parent's content box). + * @element umb-entity-frame + * @slot - Optional rich content for the tab. Falls back to the `label` property. + * @cssprop --umb-entity-frame-border-width - Thickness of the border. Defaults to `2px`. + * @cssprop --umb-entity-frame-color - Accent colour for the border and tab background. Defaults to `--uui-color-focus`. + * @cssprop --umb-entity-frame-opacity - Opacity of the border and tab. Defaults to `1`. Set to `0` on the parent and toggle to `1` on `:hover` / `:focus-within` to gate visibility. + * @augments {LitElement} + */ +@customElement('umb-entity-frame') +export class UmbEntityFrameElement extends LitElement { + /** + * Text displayed in the tab when no slot content is projected. + * @type {string} + * @attr + * @default '' + */ + @property({ type: String }) + label: string = ''; + + override render() { + return html` + +
${this.label}
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + :host { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 1; + } + + .border, + .tab { + opacity: var(--umb-entity-frame-opacity, 1); + transition: opacity 120ms ease-out; + } + + .border { + position: absolute; + inset: 0; + border: var(--umb-entity-frame-border-width, 2px) solid + var(--umb-entity-frame-color, var(--uui-color-focus)); + border-radius: var(--uui-border-radius); + border-top-right-radius: 0; + box-sizing: border-box; + pointer-events: none; + } + + .tab { + position: absolute; + bottom: 100%; + right: 0; + background: var(--umb-entity-frame-color, var(--uui-color-focus)); + color: var(--uui-color-surface, white); + padding: var(--uui-size-2) var(--uui-size-2) var(--uui-size-1); + border-radius: var(--uui-border-radius) var(--uui-border-radius) 0 0; + font-size: var(--uui-type-small-size); + line-height: 1; + pointer-events: auto; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-frame': UmbEntityFrameElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts new file mode 100644 index 000000000000..268afbdde278 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.test.ts @@ -0,0 +1,42 @@ +import { UmbEntityFrameElement } from './entity-frame.element.js'; +import { expect, fixture, html } from '@open-wc/testing'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbEntityFrameElement', () => { + let element: UmbEntityFrameElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbEntityFrameElement); + }); + + it('renders a border element', () => { + const border = element.shadowRoot!.querySelector('.border'); + expect(border).to.not.equal(null); + }); + + it('renders a tab with a default slot', () => { + const slot = element.shadowRoot!.querySelector('.tab slot'); + expect(slot).to.not.equal(null); + }); + + it('has a label property that defaults to an empty string', () => { + expect(element.label).to.equal(''); + }); + + it('renders the label inside the tab when no slot content is projected', async () => { + element.label = 'Document: Hero'; + await element.updateComplete; + const tab = element.shadowRoot!.querySelector('.tab')!; + expect(tab.textContent?.trim()).to.equal('Document: Hero'); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); From 4cf077d2f793e44986cfc0e74a88182c1504f9e7 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 12 May 2026 14:08:41 +0100 Subject: [PATCH 2/4] feat(components): export umb-entity-frame from core components barrel --- .../src/packages/core/components/entity-frame/index.ts | 2 ++ src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts | 1 + 2 files changed, 3 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts new file mode 100644 index 000000000000..8efc97ba542f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/index.ts @@ -0,0 +1,2 @@ +import './entity-frame.element.js'; +export * from './entity-frame.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index 24b71521f6ec..94d225a3bfd3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -8,6 +8,7 @@ export * from './figure-card/figure-card.element.js'; export * from './code-block/index.js'; export * from './dropdown/index.js'; export * from './entity-actions-bundle/index.js'; +export * from './entity-frame/index.js'; export * from './footer-layout/index.js'; export * from './header-app/index.js'; export * from './history/index.js'; From 67963f65fe54ef9e449f4f040a80d8224f670bc6 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 12 May 2026 14:20:03 +0100 Subject: [PATCH 3/4] feat(components): add umb-entity-frame Storybook stories --- .../entity-frame/entity-frame.element.ts | 9 +- .../entity-frame/entity-frame.stories.ts | 194 ++++++++++++++++++ 2 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts index 25f112835023..a3b4bb9b3fc0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts @@ -1,5 +1,4 @@ -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, property, LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, LitElement } from '@umbraco-cms/backoffice/external/lit'; /** * A passive overlay that frames its parent with a rounded border and shows a label tab @@ -33,8 +32,7 @@ export class UmbEntityFrameElement extends LitElement { `; } - static override styles = [ - UmbTextStyles, + static override readonly styles = [ css` :host { position: absolute; @@ -52,8 +50,7 @@ export class UmbEntityFrameElement extends LitElement { .border { position: absolute; inset: 0; - border: var(--umb-entity-frame-border-width, 2px) solid - var(--umb-entity-frame-color, var(--uui-color-focus)); + border: var(--umb-entity-frame-border-width, 2px) solid var(--umb-entity-frame-color, var(--uui-color-focus)); border-radius: var(--uui-border-radius); border-top-right-radius: 0; box-sizing: border-box; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts new file mode 100644 index 000000000000..acad6e1a636c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.stories.ts @@ -0,0 +1,194 @@ +import { html } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEntityFrameElement } from './entity-frame.element.js'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; + +import './entity-frame.element.js'; + +const meta: Meta = { + component: 'umb-entity-frame', + title: 'Generic Components/Entity Frame', + args: { + label: 'Document: Hero Banner', + }, + decorators: [(story) => html`
${story()}
`], + render: (args) => html` +
+

Entity label tab with full opacity.

+ +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Docs: Story = {}; + +export const OnHover: Story = { + render: (args) => html` + +
+

Hover this container to reveal.

+ +
+ `, +}; + +export const OnHoverOrFocus: Story = { + render: (args) => html` + +
+

Hover, or Tab into the button below.

+ Focusable child + +
+ `, +}; + +export const WithSlot: Story = { + render: () => html` +
+

Slotted content overrides the label.

+ + + Document: Hero Banner + +
+ `, +}; + +export const WrappingButton: Story = { + render: () => html` + +
+ + Edit Hero Banner + + +
+ `, +}; + +export const Nested: Story = { + render: () => html` + +
+

Outer container (both visible when hovering inner)

+ + +
+

Inner container (only inner visible without hovering outer)

+ +
+
+ `, +}; + +export const WithCustomColor: Story = { + render: () => html` +
+

Themed via --umb-entity-frame-color.

+ +
+ `, +}; + +export const ReferenceList: Story = { + render: () => { + const items = [ + { label: 'Home', color: 'maroon' }, + { label: 'About', color: 'green' }, + { label: 'Contact', color: 'blue' }, + { label: 'Blog', color: 'purple' }, + ]; + + return html` + + + ${items.map( + (item) => html` + + + + `, + )} + + `; + }, +}; From 53924ed628fe00a72f5e747d6374e21df89aeacd Mon Sep 17 00:00:00 2001 From: leekelleher Date: Tue, 12 May 2026 16:18:00 +0100 Subject: [PATCH 4/4] docs(components): note contrast expectation for umb-entity-frame-color --- .../core/components/entity-frame/entity-frame.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts index a3b4bb9b3fc0..d6d23683aa27 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-frame/entity-frame.element.ts @@ -10,7 +10,7 @@ import { css, customElement, html, property, LitElement } from '@umbraco-cms/bac * @element umb-entity-frame * @slot - Optional rich content for the tab. Falls back to the `label` property. * @cssprop --umb-entity-frame-border-width - Thickness of the border. Defaults to `2px`. - * @cssprop --umb-entity-frame-color - Accent colour for the border and tab background. Defaults to `--uui-color-focus`. + * @cssprop --umb-entity-frame-color - Accent colour for the border and tab background. Defaults to `--uui-color-focus`. Should be dark enough to maintain contrast against the white tab text. * @cssprop --umb-entity-frame-opacity - Opacity of the border and tab. Defaults to `1`. Set to `0` on the parent and toggle to `1` on `:hover` / `:focus-within` to gate visibility. * @augments {LitElement} */