@dile/ui 3.16.1 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/bottom-nav/__screenshots__/bottom-nav.spec.js/dile-bottom-nav-places-the-sheet-and-the-overlay-above-the-bar-1.png +0 -0
- package/components/bottom-nav/bottom-nav-item.js +5 -0
- package/components/bottom-nav/bottom-nav.js +6 -0
- package/components/bottom-nav/bottom-nav.spec.js +340 -0
- package/components/bottom-nav/index.js +2 -0
- package/components/bottom-nav/src/DileBottomNav.js +88 -0
- package/components/bottom-nav/src/DileBottomNavItem.js +482 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-closes-only-the-topmost-modal-when-Esc-is-pressed-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-dispatches-dile-modal-closed-and-animates-when-the-opened-property-is-set-to-false-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-does-not-close-a-blocking-modal-when-Esc-is-pressed-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-does-not-close-the-modal-below-when-the-topmost-modal-is-blocking-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-ignores-transitionend-events-coming-from-inner-elements-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-keeps-the-focus-on-content-focused-by-the-user-when-opening-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-opens-and-closes-through-the-opened-attribute-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-reflects-the-opened-property-to-the-attribute-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-after-the-close-animation-even-without-transitionend-1.png +0 -0
- package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-immediately-when-the-animation-duration-is-0s-1.png +0 -0
- package/components/modal/modal.spec.js +204 -0
- package/components/modal/src/DileModal.js +242 -27
- package/package.json +2 -2
|
@@ -43,4 +43,208 @@ describe('dile-modal', () => {
|
|
|
43
43
|
expect(el.opened).toBe(false);
|
|
44
44
|
expect(closed).toBe(true);
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it('releases the background layer immediately when the animation duration is 0s', async () => {
|
|
48
|
+
const el = await renderModal('<dile-modal style="--dile-modal-animation-duration: 0s">Modal content</dile-modal>');
|
|
49
|
+
el.open();
|
|
50
|
+
await el.updateComplete;
|
|
51
|
+
|
|
52
|
+
el.close();
|
|
53
|
+
await el.updateComplete;
|
|
54
|
+
|
|
55
|
+
const section = el.shadowRoot.querySelector('section');
|
|
56
|
+
expect(getComputedStyle(section).display).toBe('none');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('releases the background layer after the close animation even without transitionend', async () => {
|
|
60
|
+
const el = await renderModal('<dile-modal style="--dile-modal-animation-duration: 0.1s">Modal content</dile-modal>');
|
|
61
|
+
el.open();
|
|
62
|
+
await el.updateComplete;
|
|
63
|
+
|
|
64
|
+
el.close();
|
|
65
|
+
await el.updateComplete;
|
|
66
|
+
const section = el.shadowRoot.querySelector('section');
|
|
67
|
+
expect(getComputedStyle(section).pointerEvents).toBe('none');
|
|
68
|
+
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
70
|
+
await el.updateComplete;
|
|
71
|
+
expect(getComputedStyle(section).display).toBe('none');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('ignores transitionend events coming from inner elements', async () => {
|
|
75
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
76
|
+
el.open();
|
|
77
|
+
await el.updateComplete;
|
|
78
|
+
el.close();
|
|
79
|
+
await el.updateComplete;
|
|
80
|
+
|
|
81
|
+
const section = el.shadowRoot.querySelector('section');
|
|
82
|
+
const content = el.shadowRoot.querySelector('.content');
|
|
83
|
+
content.dispatchEvent(new TransitionEvent('transitionend', { bubbles: true, propertyName: 'opacity' }));
|
|
84
|
+
await el.updateComplete;
|
|
85
|
+
|
|
86
|
+
expect(section.classList.contains('opened')).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('dispatches dile-modal-closed and animates when the opened property is set to false', async () => {
|
|
90
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
91
|
+
el.opened = true;
|
|
92
|
+
await el.updateComplete;
|
|
93
|
+
|
|
94
|
+
let closed = false;
|
|
95
|
+
el.addEventListener('dile-modal-closed', () => { closed = true; });
|
|
96
|
+
|
|
97
|
+
el.opened = false;
|
|
98
|
+
await el.updateComplete;
|
|
99
|
+
|
|
100
|
+
expect(closed).toBe(true);
|
|
101
|
+
expect(el.shadowRoot.querySelector('section').classList.contains('opened')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('opens and closes through the opened attribute', async () => {
|
|
105
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
106
|
+
let closed = false;
|
|
107
|
+
el.addEventListener('dile-modal-closed', () => { closed = true; });
|
|
108
|
+
|
|
109
|
+
el.setAttribute('opened', '');
|
|
110
|
+
await el.updateComplete;
|
|
111
|
+
expect(el.opened).toBe(true);
|
|
112
|
+
|
|
113
|
+
el.removeAttribute('opened');
|
|
114
|
+
await el.updateComplete;
|
|
115
|
+
expect(el.opened).toBe(false);
|
|
116
|
+
expect(closed).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reflects the opened property to the attribute', async () => {
|
|
120
|
+
const el = await renderModal('<dile-modal opened>Modal content</dile-modal>');
|
|
121
|
+
el.close();
|
|
122
|
+
await el.updateComplete;
|
|
123
|
+
expect(el.hasAttribute('opened')).toBe(false);
|
|
124
|
+
|
|
125
|
+
el.setAttribute('opened', '');
|
|
126
|
+
await el.updateComplete;
|
|
127
|
+
expect(el.opened).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('closes a blocking modal when Esc is pressed', async () => {
|
|
131
|
+
const el = await renderModal('<dile-modal blocking>Modal content</dile-modal>');
|
|
132
|
+
el.open();
|
|
133
|
+
await el.updateComplete;
|
|
134
|
+
|
|
135
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
136
|
+
await el.updateComplete;
|
|
137
|
+
|
|
138
|
+
expect(el.opened).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('closes only the topmost modal when Esc is pressed', async () => {
|
|
142
|
+
document.body.innerHTML = '<dile-modal id="first">First</dile-modal><dile-modal id="second">Second</dile-modal>';
|
|
143
|
+
const first = document.getElementById('first');
|
|
144
|
+
const second = document.getElementById('second');
|
|
145
|
+
await first.updateComplete;
|
|
146
|
+
await second.updateComplete;
|
|
147
|
+
first.open();
|
|
148
|
+
second.open();
|
|
149
|
+
|
|
150
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
151
|
+
expect(second.opened).toBe(false);
|
|
152
|
+
expect(first.opened).toBe(true);
|
|
153
|
+
|
|
154
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
|
155
|
+
expect(first.opened).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('exposes the content as an accessible modal dialog', async () => {
|
|
159
|
+
const el = await renderModal('<dile-modal label="Settings">Modal content</dile-modal>');
|
|
160
|
+
const dialog = el.shadowRoot.querySelector('[role="dialog"]');
|
|
161
|
+
|
|
162
|
+
expect(dialog).not.toBeNull();
|
|
163
|
+
expect(dialog.getAttribute('aria-modal')).toBe('true');
|
|
164
|
+
expect(dialog.getAttribute('aria-label')).toBe('Settings');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('renders the close icon as an accessible button', async () => {
|
|
168
|
+
const el = await renderModal('<dile-modal showCloseIcon closeLabel="Cerrar">Modal content</dile-modal>');
|
|
169
|
+
el.open();
|
|
170
|
+
await el.updateComplete;
|
|
171
|
+
|
|
172
|
+
const button = el.shadowRoot.querySelector('button.closebutton');
|
|
173
|
+
expect(button.getAttribute('aria-label')).toBe('Cerrar');
|
|
174
|
+
|
|
175
|
+
button.click();
|
|
176
|
+
expect(el.opened).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('moves the focus into the modal and restores it on close', async () => {
|
|
180
|
+
document.body.innerHTML = '<button id="opener">Open</button><dile-modal>Modal content</dile-modal>';
|
|
181
|
+
const opener = document.getElementById('opener');
|
|
182
|
+
const el = document.querySelector('dile-modal');
|
|
183
|
+
await el.updateComplete;
|
|
184
|
+
|
|
185
|
+
opener.focus();
|
|
186
|
+
el.open();
|
|
187
|
+
await el.updateComplete;
|
|
188
|
+
expect(el.shadowRoot.activeElement).toBe(el.shadowRoot.querySelector('.content'));
|
|
189
|
+
|
|
190
|
+
el.close();
|
|
191
|
+
expect(document.activeElement).toBe(opener);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('keeps the focus on content focused by the user when opening', async () => {
|
|
195
|
+
const el = await renderModal('<dile-modal><input id="field"></dile-modal>');
|
|
196
|
+
const field = document.getElementById('field');
|
|
197
|
+
|
|
198
|
+
el.open();
|
|
199
|
+
await el.updateComplete;
|
|
200
|
+
field.focus();
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
202
|
+
|
|
203
|
+
expect(document.activeElement).toBe(field);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('dispatches dile-modal-opened when opened', async () => {
|
|
207
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
208
|
+
let opened = false;
|
|
209
|
+
el.addEventListener('dile-modal-opened', () => { opened = true; });
|
|
210
|
+
|
|
211
|
+
el.open();
|
|
212
|
+
expect(opened).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('becomes opaque after opening', async () => {
|
|
216
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
217
|
+
el.open();
|
|
218
|
+
await el.updateComplete;
|
|
219
|
+
|
|
220
|
+
const section = el.shadowRoot.querySelector('section');
|
|
221
|
+
await expect.poll(() => section.classList.contains('opaque')).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('locks the page scroll while any modal is opened', async () => {
|
|
225
|
+
document.body.innerHTML = '<dile-modal id="first">First</dile-modal><dile-modal id="second">Second</dile-modal>';
|
|
226
|
+
const first = document.getElementById('first');
|
|
227
|
+
const second = document.getElementById('second');
|
|
228
|
+
await second.updateComplete;
|
|
229
|
+
const root = document.documentElement;
|
|
230
|
+
|
|
231
|
+
first.open();
|
|
232
|
+
second.open();
|
|
233
|
+
expect(root.style.overflow).toBe('hidden');
|
|
234
|
+
|
|
235
|
+
second.close();
|
|
236
|
+
expect(root.style.overflow).toBe('hidden');
|
|
237
|
+
|
|
238
|
+
first.close();
|
|
239
|
+
expect(root.style.overflow).toBe('');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('unlocks the page scroll when an opened modal is removed from the document', async () => {
|
|
243
|
+
const el = await renderModal('<dile-modal>Modal content</dile-modal>');
|
|
244
|
+
el.open();
|
|
245
|
+
expect(document.documentElement.style.overflow).toBe('hidden');
|
|
246
|
+
|
|
247
|
+
el.remove();
|
|
248
|
+
expect(document.documentElement.style.overflow).toBe('');
|
|
249
|
+
});
|
|
46
250
|
});
|
|
@@ -1,33 +1,72 @@
|
|
|
1
1
|
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { ifDefined } from "lit/directives/if-defined.js";
|
|
2
3
|
import '../../icon/icon.js';
|
|
3
4
|
import { closeIcon } from '@dile/icons/index.js'
|
|
4
5
|
import { DileCloseOnEscPressed } from '../../../mixins/close-on-esc-pressed/index.js'
|
|
5
6
|
|
|
7
|
+
// Opened modals, in opening order. The last one is the topmost modal.
|
|
8
|
+
const openedModals = [];
|
|
9
|
+
// Escape keydown events already handled by a modal.
|
|
10
|
+
const handledEscEvents = new WeakSet();
|
|
11
|
+
// Inline styles of the document before locking the page scroll.
|
|
12
|
+
let scrollLockBackup = null;
|
|
13
|
+
|
|
14
|
+
// Locks the page scroll while any modal is opened.
|
|
15
|
+
function updateScrollLock() {
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
if (openedModals.length && !scrollLockBackup) {
|
|
18
|
+
const scrollbarWidth = window.innerWidth - root.clientWidth;
|
|
19
|
+
scrollLockBackup = {
|
|
20
|
+
overflow: root.style.overflow,
|
|
21
|
+
paddingRight: root.style.paddingRight,
|
|
22
|
+
};
|
|
23
|
+
root.style.overflow = 'hidden';
|
|
24
|
+
if (scrollbarWidth > 0) {
|
|
25
|
+
// Avoids the layout shift caused by the scrollbar disappearing.
|
|
26
|
+
const paddingRight = parseFloat(getComputedStyle(root).paddingRight) || 0;
|
|
27
|
+
root.style.paddingRight = `${paddingRight + scrollbarWidth}px`;
|
|
28
|
+
}
|
|
29
|
+
} else if (!openedModals.length && scrollLockBackup) {
|
|
30
|
+
root.style.overflow = scrollLockBackup.overflow;
|
|
31
|
+
root.style.paddingRight = scrollLockBackup.paddingRight;
|
|
32
|
+
scrollLockBackup = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
6
36
|
export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
7
37
|
static get properties() {
|
|
8
38
|
return {
|
|
9
|
-
_toChange: {
|
|
39
|
+
_toChange: { state: true },
|
|
10
40
|
/**
|
|
11
41
|
* If true the modal box is in opened status.
|
|
12
42
|
*/
|
|
13
|
-
opened: { type: Boolean },
|
|
43
|
+
opened: { type: Boolean, reflect: true, noAccessor: true },
|
|
14
44
|
/**
|
|
15
45
|
* If true the modal box displays a close icon
|
|
16
46
|
*/
|
|
17
47
|
showCloseIcon: { type: Boolean },
|
|
18
48
|
/**
|
|
19
|
-
* If true the modal box blocks the screen. That is, when you click in the background layer, the modal box
|
|
49
|
+
* If true the modal box blocks the screen. That is, when you click in the background layer, the modal box does not close.
|
|
20
50
|
*/
|
|
21
51
|
blocking: { type: Boolean },
|
|
52
|
+
/**
|
|
53
|
+
* Accessible name of the modal box, announced by screen readers when it opens.
|
|
54
|
+
*/
|
|
55
|
+
label: { type: String },
|
|
56
|
+
/**
|
|
57
|
+
* Accessible label of the close icon button.
|
|
58
|
+
*/
|
|
59
|
+
closeLabel: { type: String },
|
|
22
60
|
};
|
|
23
61
|
}
|
|
24
62
|
|
|
25
63
|
constructor() {
|
|
26
64
|
super();
|
|
27
65
|
this._toChange = false;
|
|
28
|
-
this.
|
|
66
|
+
this._opened = false;
|
|
29
67
|
this.showCloseIcon = false;
|
|
30
68
|
this.blocking = false;
|
|
69
|
+
this.closeLabel = 'Close';
|
|
31
70
|
}
|
|
32
71
|
|
|
33
72
|
static get styles() {
|
|
@@ -52,11 +91,10 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
52
91
|
left: 0;
|
|
53
92
|
top: 0;
|
|
54
93
|
height: 100vh;
|
|
94
|
+
height: 100dvh;
|
|
55
95
|
width: 100vw;
|
|
56
96
|
display: none;
|
|
57
97
|
transition: opacity var(--dile-modal-animation-duration, 0.3s) ease-in;
|
|
58
|
-
-webkit-transition: opacity var(--dile-modal-animation-duration, 0.3s)
|
|
59
|
-
ease-in;
|
|
60
98
|
align-items: center;
|
|
61
99
|
justify-content: center;
|
|
62
100
|
z-index: var(--dile-modal-z-index, 100);
|
|
@@ -87,13 +125,13 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
87
125
|
}
|
|
88
126
|
article {
|
|
89
127
|
overflow: auto;
|
|
90
|
-
max-height: 100%;
|
|
91
128
|
padding-right: var(--dile-modal-content-padding, 1em);
|
|
92
129
|
height: var(--dile-modal-height, auto);
|
|
93
130
|
max-height: var(--dile-modal-max-height, 90vh);
|
|
94
131
|
}
|
|
95
132
|
.transparent {
|
|
96
133
|
opacity: 0;
|
|
134
|
+
pointer-events: none;
|
|
97
135
|
}
|
|
98
136
|
.opaque {
|
|
99
137
|
opacity: 1;
|
|
@@ -102,12 +140,20 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
102
140
|
.opened {
|
|
103
141
|
display: flex !important;
|
|
104
142
|
}
|
|
105
|
-
|
|
143
|
+
.content:focus {
|
|
144
|
+
outline: none;
|
|
145
|
+
}
|
|
146
|
+
.closebutton {
|
|
106
147
|
position: absolute;
|
|
107
|
-
display: inline-
|
|
148
|
+
display: inline-flex;
|
|
108
149
|
top: var(--dile-modal-close-icon-top, 5px);
|
|
109
150
|
right: var(--dile-modal-close-icon-right, 18px);
|
|
110
151
|
z-index: 1002;
|
|
152
|
+
padding: 0;
|
|
153
|
+
margin: 0;
|
|
154
|
+
border: none;
|
|
155
|
+
background: none;
|
|
156
|
+
color: inherit;
|
|
111
157
|
cursor: var(--dile-modal-close-icon-cursor, pointer);
|
|
112
158
|
}
|
|
113
159
|
.contentIconSeparation {
|
|
@@ -125,9 +171,17 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
125
171
|
@transitionend="${this.animationEnd}"
|
|
126
172
|
id="backgroundmodal"
|
|
127
173
|
>
|
|
128
|
-
<div
|
|
174
|
+
<div
|
|
175
|
+
class="content"
|
|
176
|
+
role="dialog"
|
|
177
|
+
aria-modal="true"
|
|
178
|
+
aria-label="${ifDefined(this.label)}"
|
|
179
|
+
tabindex="-1"
|
|
180
|
+
>
|
|
129
181
|
${this.showCloseIcon
|
|
130
|
-
? html`<
|
|
182
|
+
? html`<button type="button" class="closebutton" aria-label="${this.closeLabel}" @click="${this.close}">
|
|
183
|
+
<dile-icon .icon="${closeIcon}" class="closeicon"></dile-icon>
|
|
184
|
+
</button>`
|
|
131
185
|
: ""}
|
|
132
186
|
<article class="${this.showCloseIcon ? "contentIconSeparation" : ""}">
|
|
133
187
|
<slot></slot>
|
|
@@ -153,12 +207,29 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
153
207
|
return '';
|
|
154
208
|
}
|
|
155
209
|
|
|
210
|
+
get opened() {
|
|
211
|
+
return this._opened;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The opened property can be changed by open() / close(), or directly
|
|
215
|
+
// by the property or the attribute. All ways run the same transitions.
|
|
216
|
+
set opened(value) {
|
|
217
|
+
const oldValue = this._opened;
|
|
218
|
+
value = Boolean(value);
|
|
219
|
+
if (value === oldValue) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this._opened = value;
|
|
223
|
+
this.requestUpdate('opened', oldValue);
|
|
224
|
+
if (value) {
|
|
225
|
+
this._startOpening();
|
|
226
|
+
} else {
|
|
227
|
+
this._startClosing();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
156
231
|
open() {
|
|
157
232
|
this.opened = true;
|
|
158
|
-
this._toChange = true;
|
|
159
|
-
setTimeout(() => {
|
|
160
|
-
this._toChange = false;
|
|
161
|
-
}, 50);
|
|
162
233
|
}
|
|
163
234
|
|
|
164
235
|
_backgroundModalClick(e) {
|
|
@@ -175,22 +246,166 @@ export class DileModal extends DileCloseOnEscPressed(LitElement) {
|
|
|
175
246
|
}
|
|
176
247
|
|
|
177
248
|
close() {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
249
|
+
this.opened = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_startOpening() {
|
|
253
|
+
this._clearCloseTimeout();
|
|
254
|
+
this._removeFromOpenedModals();
|
|
255
|
+
if (this.isConnected) {
|
|
256
|
+
this._addToOpenedModals();
|
|
257
|
+
}
|
|
258
|
+
this._toChange = true;
|
|
259
|
+
this._cancelOpenFrame();
|
|
260
|
+
// Waits for the layer to be painted with opacity 0, so the fade-in transition runs.
|
|
261
|
+
this.updateComplete.then(() => {
|
|
262
|
+
this._openFrame = requestAnimationFrame(() => {
|
|
263
|
+
this._openFrame = requestAnimationFrame(() => {
|
|
264
|
+
this._openFrame = null;
|
|
265
|
+
if (this.opened) {
|
|
266
|
+
this._toChange = false;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
this._previousFocus = this._getDeepActiveElement();
|
|
272
|
+
this.updateComplete.then(() => this._focusContent());
|
|
273
|
+
this.dispatchEvent(
|
|
274
|
+
new CustomEvent("dile-modal-opened", {
|
|
275
|
+
bubbles: true,
|
|
276
|
+
composed: true,
|
|
277
|
+
detail: this,
|
|
278
|
+
})
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_cancelOpenFrame() {
|
|
283
|
+
if (this._openFrame) {
|
|
284
|
+
cancelAnimationFrame(this._openFrame);
|
|
285
|
+
this._openFrame = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Moves the focus inside the modal, unless the content already took it.
|
|
290
|
+
_focusContent() {
|
|
291
|
+
if (!this.opened || this.matches(':focus-within')) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.shadowRoot.querySelector('.content')?.focus({ preventScroll: true });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Returns the focus to the element that had it before opening the modal.
|
|
298
|
+
_restoreFocus() {
|
|
299
|
+
const previousFocus = this._previousFocus;
|
|
300
|
+
this._previousFocus = null;
|
|
301
|
+
const active = this._getDeepActiveElement();
|
|
302
|
+
const focusLost = !active || active === document.body;
|
|
303
|
+
if (!previousFocus?.isConnected || !(focusLost || this.matches(':focus-within'))) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
previousFocus.focus({ preventScroll: true });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_getDeepActiveElement() {
|
|
310
|
+
let active = document.activeElement;
|
|
311
|
+
while (active?.shadowRoot?.activeElement) {
|
|
312
|
+
active = active.shadowRoot.activeElement;
|
|
313
|
+
}
|
|
314
|
+
return active;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_startClosing() {
|
|
318
|
+
this._cancelOpenFrame();
|
|
319
|
+
this._removeFromOpenedModals();
|
|
320
|
+
this._toChange = true;
|
|
321
|
+
this._restoreFocus();
|
|
322
|
+
this._scheduleCloseEnd();
|
|
323
|
+
this.dispatchEvent(
|
|
324
|
+
new CustomEvent("dile-modal-closed", {
|
|
325
|
+
bubbles: true,
|
|
326
|
+
composed: true,
|
|
327
|
+
detail: this,
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_removeFromOpenedModals() {
|
|
333
|
+
const index = openedModals.indexOf(this);
|
|
334
|
+
if (index !== -1) {
|
|
335
|
+
openedModals.splice(index, 1);
|
|
336
|
+
updateScrollLock();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_addToOpenedModals() {
|
|
341
|
+
if (!openedModals.includes(this)) {
|
|
342
|
+
openedModals.push(this);
|
|
343
|
+
updateScrollLock();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Only the topmost modal reacts to the Esc key.
|
|
348
|
+
escClose(e) {
|
|
349
|
+
if (e.key !== "Escape" || !this.opened || handledEscEvents.has(e)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (openedModals[openedModals.length - 1] !== this) {
|
|
353
|
+
return;
|
|
188
354
|
}
|
|
355
|
+
handledEscEvents.add(e);
|
|
356
|
+
this.close();
|
|
189
357
|
}
|
|
190
358
|
|
|
191
|
-
animationEnd() {
|
|
359
|
+
animationEnd(e) {
|
|
360
|
+
if (e && (e.target !== e.currentTarget || e.propertyName !== 'opacity')) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this._clearCloseTimeout();
|
|
192
364
|
this._toChange = false;
|
|
193
365
|
}
|
|
194
366
|
|
|
195
|
-
|
|
367
|
+
connectedCallback() {
|
|
368
|
+
super.connectedCallback();
|
|
369
|
+
if (this.opened) {
|
|
370
|
+
this._addToOpenedModals();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
disconnectedCallback() {
|
|
375
|
+
super.disconnectedCallback();
|
|
376
|
+
this._clearCloseTimeout();
|
|
377
|
+
this._cancelOpenFrame();
|
|
378
|
+
this._removeFromOpenedModals();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// transitionend is not fired when the transition duration is 0 (or the
|
|
382
|
+
// transition is interrupted), so the background layer is also released by a timer.
|
|
383
|
+
_scheduleCloseEnd() {
|
|
384
|
+
this._clearCloseTimeout();
|
|
385
|
+
const duration = this._getAnimationDuration();
|
|
386
|
+
if (duration <= 0) {
|
|
387
|
+
this.animationEnd();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this._closeTimeout = setTimeout(() => this.animationEnd(), duration + 50);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_clearCloseTimeout() {
|
|
394
|
+
if (this._closeTimeout) {
|
|
395
|
+
clearTimeout(this._closeTimeout);
|
|
396
|
+
this._closeTimeout = null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_getAnimationDuration() {
|
|
401
|
+
const section = this.shadowRoot?.querySelector('section');
|
|
402
|
+
if (!section) {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
const durations = getComputedStyle(section).transitionDuration.split(',');
|
|
406
|
+
return Math.max(...durations.map((value) => {
|
|
407
|
+
const time = parseFloat(value) || 0;
|
|
408
|
+
return value.trim().endsWith('ms') ? time : time * 1000;
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
196
411
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "ea92314b81f3274d6f843b7a926fc528f1a13879"
|
|
32
32
|
}
|