Skip to content

Commit 0021e92

Browse files
Modals (#435)
Co-authored-by: Niels Lyngsø <[email protected]>
1 parent b42a474 commit 0021e92

16 files changed

+846
-1
lines changed

.storybook/main.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
const tsconfigPaths = require('vite-tsconfig-paths').default;
22

33
module.exports = {
4-
stories: ['../packages/**/*.story.ts', '../stories/**/*.story.ts'],
4+
stories: [
5+
'../packages/**/*.story.ts',
6+
'../stories/**/*.story.ts',
7+
'../packages/**/*.story.mdx',
8+
'../stories/**/*.story.mdx',
9+
],
510
addons: [
611
'@storybook/addon-essentials',
712
'@storybook/addon-links',

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uui-modal/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# uui-modal
2+
3+
![npm](https://img.shields.io/npm/v/@umbraco-ui/uui-modal?logoColor=%231B264F)
4+
5+
Umbraco style modal component.
6+
7+
## Installation
8+
9+
### ES imports
10+
11+
```zsh
12+
npm i @umbraco-ui/uui-modal
13+
```
14+
15+
Import the registration of `<uui-modal>` via:
16+
17+
```javascript
18+
import '@umbraco-ui/uui-modal';
19+
```
20+
21+
When looking to leverage the `UUIModalElement` base class as a type and/or for extension purposes, do so via:
22+
23+
```javascript
24+
import { UUIModalElement } from '@umbraco-ui/uui-modal';
25+
```
26+
27+
## Usage
28+
29+
```html
30+
<uui-modal></uui-modal>
31+
```

packages/uui-modal/lib/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './uui-modal.element';
2+
export * from './uui-modal-container';
3+
export * from './uui-modal-dialog.element';
4+
export * from './uui-modal-sidebar.element';
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { LitElement, css, html, TemplateResult } from 'lit';
2+
import { customElement, state } from 'lit/decorators.js';
3+
import './uui-modal-container';
4+
import { ref, createRef } from 'lit/directives/ref.js';
5+
import { UUIModalElement } from './uui-modal.element';
6+
7+
@customElement('modal-example')
8+
export class ModalExampleElement extends LitElement {
9+
@state()
10+
private _modals: TemplateResult<1>[] = [];
11+
12+
#addDialog() {
13+
const modalRef = createRef<UUIModalElement>();
14+
15+
this._modals.push(html`
16+
<uui-modal-dialog ${ref(modalRef)}>
17+
<uui-dialog-layout style="max-width: 500px" headline="My dialog">
18+
<p>Dialog content goes here</p>
19+
${this.#renderButtons()}
20+
<uui-button
21+
@click=${() => modalRef.value?.close()}
22+
slot="actions"
23+
look="primary">
24+
close
25+
</uui-button>
26+
</uui-dialog-layout>
27+
</uui-modal-dialog>
28+
`);
29+
30+
this.requestUpdate('_modals');
31+
}
32+
33+
#addSidebar(size: string) {
34+
const modalRef = createRef<UUIModalElement>();
35+
36+
this._modals.push(html`
37+
<uui-modal-sidebar ${ref(modalRef)} size=${size}>
38+
<div
39+
style="background: var(--uui-color-background); display: flex; flex-direction: column; height: 100%;">
40+
<uui-box headline="Sidebar" style="margin: 12px">
41+
<p>Sidebar content goes here</p>
42+
${this.#renderButtons()}
43+
</uui-box>
44+
<div class="sidebar-buttons">
45+
<uui-button
46+
@click=${() => modalRef.value?.close()}
47+
slot="actions"
48+
look="primary">
49+
close
50+
</uui-button>
51+
</div>
52+
</div>
53+
</uui-modal-sidebar>
54+
`);
55+
56+
this.requestUpdate('_modals');
57+
}
58+
59+
#renderButtons() {
60+
return html`
61+
<div
62+
style="width: max-content; border: 1px solid; padding: 16px; display: flex; flex-direction: column;">
63+
<strong>Dialog</strong>
64+
<uui-button look="secondary" @click=${this.#addDialog}>Open</uui-button>
65+
<hr />
66+
<strong>Sidebars</strong>
67+
<div>
68+
<uui-button
69+
look="secondary"
70+
@click=${() => this.#addSidebar('small')}>
71+
small
72+
</uui-button>
73+
<uui-button
74+
look="secondary"
75+
@click=${() => this.#addSidebar('medium')}>
76+
medium
77+
</uui-button>
78+
<uui-button
79+
look="secondary"
80+
@click=${() => this.#addSidebar('large')}>
81+
large
82+
</uui-button>
83+
<uui-button look="secondary" @click=${() => this.#addSidebar('full')}>
84+
full
85+
</uui-button>
86+
</div>
87+
</div>
88+
`;
89+
}
90+
91+
render() {
92+
return html` ${this.#renderButtons()}
93+
<uui-modal-container>${this._modals}</uui-modal-container>`;
94+
}
95+
static styles = css`
96+
.sidebar-buttons {
97+
margin-top: auto;
98+
display: flex;
99+
align-items: center;
100+
justify-content: end;
101+
padding: 16px;
102+
background: var(--uui-color-surface);
103+
box-shadow: var(--uui-shadow-depth-4);
104+
}
105+
`;
106+
}
107+
108+
declare global {
109+
interface HTMLElementTagNameMap {
110+
'modal-example': ModalExampleElement;
111+
}
112+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { LitElement, css, html } from 'lit';
2+
import { customElement, property, query, state } from 'lit/decorators.js';
3+
import { UUIModalSidebarElement } from './uui-modal-sidebar.element';
4+
import { UUIModalElement } from './uui-modal.element';
5+
@customElement('uui-modal-container')
6+
export class UUIModalContainerElement extends LitElement {
7+
@query('slot')
8+
modalSlot?: HTMLSlotElement;
9+
10+
@state()
11+
private _modals?: Array<UUIModalElement>;
12+
13+
@state()
14+
private _sidebars?: Array<UUIModalSidebarElement>;
15+
16+
@property({ type: Number })
17+
sidebarGap = 64;
18+
19+
@property({ type: Number })
20+
transitionDurationMS = 250;
21+
22+
constructor() {
23+
super();
24+
this.addEventListener('close', this.#onClose);
25+
this.style.setProperty(
26+
'--uui-modal-transition-duration',
27+
this.transitionDurationMS + 'ms'
28+
);
29+
}
30+
31+
#onSlotChange = () => {
32+
this._modals =
33+
(this.modalSlot
34+
?.assignedElements({ flatten: true })
35+
.filter(
36+
el => el instanceof UUIModalElement
37+
) as Array<UUIModalElement>) ?? [];
38+
39+
this._sidebars = this._modals.filter(
40+
el => el instanceof UUIModalSidebarElement
41+
) as Array<UUIModalSidebarElement>;
42+
43+
if (this._modals.length === 0) {
44+
this.removeAttribute('backdrop');
45+
return;
46+
}
47+
48+
this.#updateModals();
49+
this.#updateSidebars();
50+
};
51+
52+
#onClose = () => {
53+
if (!this._modals || this._modals.length <= 1) {
54+
this.removeAttribute('backdrop');
55+
return;
56+
}
57+
58+
this.#updateModals();
59+
this.#updateSidebars();
60+
};
61+
62+
#updateModals() {
63+
this.setAttribute('backdrop', '');
64+
65+
const reverse =
66+
this._modals?.filter(modal => !modal.isClosing).reverse() ?? [];
67+
68+
//set index to all modals, the one in front is 0
69+
reverse?.forEach((modal, index) => {
70+
modal.index = index;
71+
modal.transitionDuration = this.transitionDurationMS;
72+
});
73+
74+
//set unique-index on all modals based on which modal of the same type it is, the one in front is 0.
75+
reverse?.forEach(modal => {
76+
const sameType = reverse?.filter(
77+
m => m.constructor.name === modal.constructor.name
78+
);
79+
80+
modal.uniqueIndex = sameType?.indexOf(modal) ?? 0;
81+
});
82+
}
83+
84+
#updateSidebars() {
85+
requestAnimationFrame(() => {
86+
let sidebarOffset = 0;
87+
const reversed =
88+
this._sidebars?.filter(modal => !modal.isClosing).reverse() ?? [];
89+
90+
for (let i = 0; i < reversed.length; i++) {
91+
const sidebar = reversed[i];
92+
const nextSidebar = reversed[i + 1];
93+
sidebar.style.setProperty('--uui-modal-offset', sidebarOffset + 'px');
94+
95+
// Stop the calculations if the next sidebar is hidden
96+
if (nextSidebar?.hasAttribute('hide')) break;
97+
98+
//TODO: is there a better way to get the width of the sidebar?
99+
const currentWidth =
100+
sidebar.shadowRoot?.querySelector('dialog')?.getBoundingClientRect()
101+
.width ?? 0;
102+
103+
//TODO: is there a better way to get the width of the sidebar?
104+
const nextWidth =
105+
nextSidebar?.shadowRoot
106+
?.querySelector('dialog')
107+
?.getBoundingClientRect().width ?? 0;
108+
const distance =
109+
currentWidth + sidebarOffset + this.sidebarGap - nextWidth;
110+
sidebarOffset = distance > 0 ? distance : 0;
111+
}
112+
});
113+
}
114+
115+
render() {
116+
return html`<slot @slotchange=${this.#onSlotChange}></slot>`;
117+
}
118+
static styles = css`
119+
:host {
120+
position: fixed;
121+
--uui-modal-color-backdrop: rgba(0, 0, 0, 0.5);
122+
}
123+
:host::after {
124+
content: '';
125+
position: fixed;
126+
inset: 0;
127+
background-color: var(--uui-modal-color-backdrop, rgba(0, 0, 0, 0.5));
128+
opacity: 0;
129+
pointer-events: none;
130+
transition: opacity var(--uui-modal-transition-duration, 250ms);
131+
}
132+
:host([backdrop])::after {
133+
opacity: 1;
134+
}
135+
`;
136+
}
137+
138+
declare global {
139+
interface HTMLElementTagNameMap {
140+
'uui-modal-container': UUIModalContainerElement;
141+
}
142+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { css, html } from 'lit';
2+
import { customElement } from 'lit/decorators.js';
3+
import { UUIModalElement } from './uui-modal.element';
4+
5+
@customElement('uui-modal-dialog')
6+
export class UUIModalDialogElement extends UUIModalElement {
7+
render() {
8+
return html`
9+
<dialog>
10+
<slot></slot>
11+
</dialog>
12+
`;
13+
}
14+
15+
static styles = [
16+
...UUIModalElement.styles,
17+
css`
18+
dialog {
19+
margin: auto;
20+
max-width: 100%;
21+
max-height: 100%;
22+
border-radius: 12px;
23+
}
24+
:host([index='0']) dialog {
25+
box-shadow: var(--uui-shadow-depth-5);
26+
}
27+
:host(:not([index='0'])) dialog {
28+
outline: 1px solid rgba(0, 0, 0, 0.1);
29+
}
30+
`,
31+
];
32+
}
33+
34+
declare global {
35+
interface HTMLElementTagNameMap {
36+
'uui-modal-dialog': UUIModalDialogElement;
37+
}
38+
}

0 commit comments

Comments
 (0)