Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit 2273af3

Browse files
authored
Merge pull request #1507 from enkelmedia/1506-custom-modal
Feature/Proposal: Added support for element factory for modal manager context
2 parents 0616a43 + 3f5bc93 commit 2273af3

File tree

11 files changed

+244
-45
lines changed

11 files changed

+244
-45
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { EXAMPLE_MODAL_TOKEN, type ExampleModalData, type ExampleModalResult } from './example-modal-token.js';
2+
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
3+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
4+
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
5+
import './example-custom-modal-element.element.js';
6+
7+
@customElement('example-custom-modal-dashboard')
8+
export class UmbExampleCustomModalDashboardElement extends UmbLitElement {
9+
10+
#modalManagerContext? : typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
11+
12+
constructor() {
13+
super();
14+
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT,(instance)=>{
15+
this.#modalManagerContext = instance;
16+
})
17+
}
18+
19+
#onOpenModal(){
20+
this.#modalManagerContext?.open(this,EXAMPLE_MODAL_TOKEN,{})
21+
}
22+
23+
override render() {
24+
return html`
25+
<uui-box>
26+
<p>Open the custom modal</p>
27+
<uui-button look="primary" @click=${this.#onOpenModal}>Open Modal</uui-button>
28+
</uui-box>
29+
`;
30+
}
31+
32+
static override styles = [css`
33+
:host{
34+
display:block;
35+
padding:20px;
36+
}
37+
`];
38+
}
39+
40+
export default UmbExampleCustomModalDashboardElement
41+
42+
declare global {
43+
interface HTMLElementTagNameMap {
44+
'example-custom-modal-dashboard': UmbExampleCustomModalDashboardElement;
45+
}
46+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { css, html } from "@umbraco-cms/backoffice/external/lit";
2+
import { defineElement, UUIModalElement } from "@umbraco-cms/backoffice/external/uui";
3+
4+
/**
5+
* This class defines a custom design for the modal it self, in the same was as
6+
* UUIModalSidebarElement and UUIModalDialogElement.
7+
*/
8+
@defineElement('example-modal-element')
9+
export class UmbExampleCustomModalElement extends UUIModalElement {
10+
override render() {
11+
return html`
12+
<dialog>
13+
<h2>Custom Modal-wrapper</h2>
14+
<slot></slot>
15+
</dialog>
16+
`;
17+
}
18+
19+
static override styles = [
20+
...UUIModalElement.styles,
21+
css`
22+
dialog {
23+
width:100%;
24+
height:100%;
25+
max-width: 100%;
26+
max-height: 100%;
27+
top:0;
28+
left:0;
29+
right:0;
30+
bottom:0;
31+
background:#fff;
32+
}
33+
:host([index='0']) dialog {
34+
box-shadow: var(--uui-shadow-depth-5);
35+
}
36+
:host(:not([index='0'])) dialog {
37+
outline: 1px solid rgba(0, 0, 0, 0.1);
38+
}
39+
40+
`,
41+
];
42+
}
43+
44+
export default UmbExampleCustomModalElement;
45+
46+
declare global {
47+
interface HTMLElementTagNameMap {
48+
'example-modal-element': UmbExampleCustomModalElement;
49+
}
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { UmbModalToken } from "@umbraco-cms/backoffice/modal";
2+
3+
export interface ExampleModalData {
4+
unique: string | null;
5+
}
6+
7+
export interface ExampleModalResult {
8+
text : string;
9+
}
10+
11+
export const EXAMPLE_MODAL_TOKEN = new UmbModalToken<
12+
ExampleModalData,
13+
ExampleModalResult
14+
>('example.modal.custom.element', {
15+
modal : {
16+
type : 'custom',
17+
element: () => import('./example-custom-modal-element.element.js'),
18+
}
19+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { ExampleModalData, ExampleModalResult } from './example-modal-token.js';
2+
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
3+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
4+
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
5+
import './example-custom-modal-element.element.js';
6+
7+
@customElement('example-modal-view')
8+
export class UmbExampleModalViewElement extends UmbLitElement {
9+
10+
@property({ attribute: false })
11+
public modalContext?: UmbModalContext<ExampleModalData, ExampleModalResult>;
12+
13+
onClickDone(){
14+
this.modalContext?.submit();
15+
}
16+
17+
override render() {
18+
return html`
19+
<div id="modal">
20+
<p>Example content of custom modal element</p>
21+
<uui-button look="primary" label="Submit modal" @click=${() => this.onClickDone()}></uui-button>
22+
</div>
23+
`;
24+
}
25+
26+
static override styles = [css`
27+
:host {
28+
background: #eaeaea;
29+
display: block;
30+
box-sizing:border-box;
31+
}
32+
33+
#modal {
34+
box-sizing:border-box;
35+
}
36+
37+
p {
38+
margin:0;
39+
padding:0;
40+
}
41+
42+
`];
43+
}
44+
45+
export default UmbExampleModalViewElement
46+
47+
declare global {
48+
interface HTMLElementTagNameMap {
49+
'example-modal-view': UmbExampleModalViewElement;
50+
}
51+
}

examples/custom-modal/index.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard';
2+
import type { ManifestModal } from '@umbraco-cms/backoffice/modal';
3+
4+
const demoModal : ManifestModal = {
5+
type: 'modal',
6+
name: 'Example Custom Modal Element',
7+
alias: 'example.modal.custom.element',
8+
js: () => import('./example-modal-view.element.js'),
9+
}
10+
11+
const demoModalsDashboard : ManifestDashboard = {
12+
type: 'dashboard',
13+
name: 'Example Custom Modal Dashboard',
14+
alias: 'example.dashboard.custom.modal.element',
15+
element: () => import('./example-custom-modal-dashboard.element.js'),
16+
weight: 900,
17+
meta: {
18+
label: 'Custom Modal',
19+
pathname: 'custom-modal',
20+
},
21+
conditions : [
22+
{
23+
alias: 'Umb.Condition.SectionAlias',
24+
match: 'Umb.Section.Content'
25+
}
26+
]
27+
}
28+
29+
export default [demoModal,demoModalsDashboard];

examples/modal-routed/index.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
1-
import type { ManifestDashboard, ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
2-
3-
// const section : ManifestSection = {
4-
// type: "section",
5-
// alias: 'demo.section',
6-
// name: "Demo Section",
7-
// meta: {
8-
// label: "Demo",
9-
// pathname: "demo"
10-
// }
11-
// }
1+
import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard';
2+
import type { ManifestModal } from '@umbraco-cms/backoffice/modal';
123

134
const dashboard: ManifestDashboard = {
145
type: 'dashboard',

src/packages/core/components/backoffice-modal-container/backoffice-modal-container.element.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
1313
@state()
1414
_modals: Array<UmbModalContext> = [];
1515

16-
@property({ reflect: true, attribute: 'fill-background' })
16+
@property({ type: Boolean, reflect: true, attribute: 'fill-background' })
1717
fillBackground = false;
1818

1919
private _modalManager?: UmbModalManagerContext;
@@ -41,7 +41,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
4141
* @param modals
4242
*/
4343
#createModalElements(modals: Array<UmbModalContext>) {
44-
this.removeAttribute('fill-background');
44+
this.fillBackground = false;
4545
const oldValue = this._modals;
4646
this._modals = modals;
4747

@@ -58,26 +58,26 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
5858
return;
5959
}
6060

61-
this._modals.forEach((modal) => {
62-
if (this._modalElementMap.has(modal.key)) return;
61+
this._modals.forEach(async (modalContext) => {
62+
if (this._modalElementMap.has(modalContext.key)) return;
6363

6464
const modalElement = new UmbModalElement();
65-
modalElement.modalContext = modal;
65+
await modalElement.init(modalContext);
6666

67-
modalElement.element?.addEventListener('close-end', this.#onCloseEnd.bind(this, modal.key));
68-
modal.addEventListener('umb:destroy', this.#onCloseEnd.bind(this, modal.key));
67+
modalElement.element?.addEventListener('close-end', this.#onCloseEnd.bind(this, modalContext.key));
68+
modalContext.addEventListener('umb:destroy', this.#onCloseEnd.bind(this, modalContext.key));
6969

70-
this._modalElementMap.set(modal.key, modalElement);
70+
this._modalElementMap.set(modalContext.key, modalElement);
7171

7272
// If any of the modals are fillBackground, set the fillBackground property to true
73-
if (modal.backdropBackground) {
73+
if (modalContext.backdropBackground) {
7474
this.fillBackground = true;
7575
this.shadowRoot
7676
?.getElementById('container')
77-
?.style.setProperty('--backdrop-background', modal.backdropBackground);
77+
?.style.setProperty('--backdrop-background', modalContext.backdropBackground);
7878
}
7979

80-
this.requestUpdate();
80+
this.requestUpdate('_modalElementMap');
8181
});
8282
}
8383

src/packages/core/modal/component/modal.element.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import { html, customElement } from '@umbraco-cms/backoffice/external/lit';
99
import { UmbBasicState, type UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
1010
import {
1111
UUIModalCloseEvent,
12+
type UUIModalElement,
1213
type UUIDialogElement,
1314
type UUIModalDialogElement,
1415
type UUIModalSidebarElement,
1516
} from '@umbraco-cms/backoffice/external/uui';
1617
import { UMB_ROUTE_CONTEXT, type UmbRouterSlotElement } from '@umbraco-cms/backoffice/router';
17-
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
18+
import { createExtensionElement, loadManifestElement } from '@umbraco-cms/backoffice/extension-api';
1819
import type { UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api';
1920
import {
2021
UMB_CONTEXT_REQUEST_EVENT_TYPE,
@@ -25,22 +26,8 @@ import {
2526
@customElement('umb-modal')
2627
export class UmbModalElement extends UmbLitElement {
2728
#modalContext?: UmbModalContext;
28-
public get modalContext(): UmbModalContext | undefined {
29-
return this.#modalContext;
30-
}
31-
public set modalContext(value: UmbModalContext | undefined) {
32-
if (this.#modalContext === value) return;
33-
this.#modalContext = value;
34-
35-
if (!value) {
36-
this.destroy();
37-
return;
38-
}
39-
40-
this.#createModalElement();
41-
}
4229

43-
public element?: UUIModalDialogElement | UUIModalSidebarElement;
30+
public element?: UUIModalDialogElement | UUIModalSidebarElement | UUIModalElement;
4431

4532
#innerElement = new UmbBasicState<HTMLElement | undefined>(undefined);
4633

@@ -52,11 +39,17 @@ export class UmbModalElement extends UmbLitElement {
5239
this.#modalContext?.reject({ type: 'close' });
5340
};
5441

55-
#createModalElement() {
56-
if (!this.#modalContext) return;
42+
async init(modalContext: UmbModalContext | undefined) {
43+
if (this.#modalContext === modalContext) return;
44+
this.#modalContext = modalContext;
45+
46+
if (!this.#modalContext) {
47+
this.destroy();
48+
return;
49+
}
5750

5851
this.#modalContext.addEventListener('umb:destroy', this.#onContextDestroy);
59-
this.element = this.#createContainerElement();
52+
this.element = await this.#createContainerElement();
6053

6154
// Makes sure that the modal triggers the reject of the context promise when it is closed by pressing escape.
6255
this.element.addEventListener(UUIModalCloseEvent, this.#onClose);
@@ -113,7 +106,12 @@ export class UmbModalElement extends UmbLitElement {
113106
provider.hostConnected();
114107
}
115108

116-
#createContainerElement() {
109+
async #createContainerElement() {
110+
if (this.#modalContext!.type == 'custom' && this.#modalContext?.element) {
111+
const customWrapperElementCtor = await loadManifestElement(this.#modalContext.element);
112+
return new customWrapperElementCtor!();
113+
}
114+
117115
return this.#modalContext!.type === 'sidebar' ? this.#createSidebarElement() : this.#createDialogElement();
118116
}
119117

src/packages/core/modal/context/modal-manager.context.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
import type { UmbModalToken } from '../token/modal-token.js';
22
import { UmbModalContext, type UmbModalContextClassArgs } from './modal.context.js';
3-
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
3+
import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
44
import { UmbBasicState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api';
55
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
66
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
77
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
8+
import type { ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api';
89

9-
export type UmbModalType = 'dialog' | 'sidebar';
10+
export type UmbModalType = 'dialog' | 'sidebar' | 'custom';
1011

1112
export interface UmbModalConfig {
1213
key?: string;
1314
type?: UmbModalType;
1415
size?: UUIModalSidebarSize;
1516

17+
/**
18+
* Used to provide a custom modal element to replace the default uui-modal-dialog or uui-modal-sidebar
19+
*/
20+
element?: ElementLoaderProperty<UUIModalElement>;
21+
1622
/**
1723
* Set the background property of the modal backdrop
1824
*/

0 commit comments

Comments
 (0)