@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.
Files changed (20) hide show
  1. 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
  2. package/components/bottom-nav/bottom-nav-item.js +5 -0
  3. package/components/bottom-nav/bottom-nav.js +6 -0
  4. package/components/bottom-nav/bottom-nav.spec.js +340 -0
  5. package/components/bottom-nav/index.js +2 -0
  6. package/components/bottom-nav/src/DileBottomNav.js +88 -0
  7. package/components/bottom-nav/src/DileBottomNavItem.js +482 -0
  8. package/components/modal/__screenshots__/modal.spec.js/dile-modal-closes-only-the-topmost-modal-when-Esc-is-pressed-1.png +0 -0
  9. 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
  10. package/components/modal/__screenshots__/modal.spec.js/dile-modal-does-not-close-a-blocking-modal-when-Esc-is-pressed-1.png +0 -0
  11. 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
  12. package/components/modal/__screenshots__/modal.spec.js/dile-modal-ignores-transitionend-events-coming-from-inner-elements-1.png +0 -0
  13. package/components/modal/__screenshots__/modal.spec.js/dile-modal-keeps-the-focus-on-content-focused-by-the-user-when-opening-1.png +0 -0
  14. package/components/modal/__screenshots__/modal.spec.js/dile-modal-opens-and-closes-through-the-opened-attribute-1.png +0 -0
  15. package/components/modal/__screenshots__/modal.spec.js/dile-modal-reflects-the-opened-property-to-the-attribute-1.png +0 -0
  16. package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-after-the-close-animation-even-without-transitionend-1.png +0 -0
  17. package/components/modal/__screenshots__/modal.spec.js/dile-modal-releases-the-background-layer-immediately-when-the-animation-duration-is-0s-1.png +0 -0
  18. package/components/modal/modal.spec.js +204 -0
  19. package/components/modal/src/DileModal.js +242 -27
  20. package/package.json +2 -2
@@ -0,0 +1,482 @@
1
+ import { html, css, LitElement } from "lit";
2
+ import { ifDefined } from "lit/directives/if-defined.js";
3
+ import { styleMap } from "lit/directives/style-map.js";
4
+ import { DileSelectableItem } from "../../../mixins/selectable/index.js";
5
+ import { DileCloseOnEscPressed } from "../../../mixins/close-on-esc-pressed/index.js";
6
+ import { DileOverlay } from "../../../mixins/overlay/index.js";
7
+ import '@dile/iconlib/dile-iconlib.js';
8
+ // Glyph of the default close icon.
9
+ import '@dile/iconlib/lucide-icons/x.js';
10
+
11
+ // Number of opened sheets. The page scroll is locked while any sheet is opened.
12
+ let openedSheets = 0;
13
+ // Inline styles of the document before locking the page scroll.
14
+ let scrollLockBackup = null;
15
+
16
+ function updateScrollLock() {
17
+ const root = document.documentElement;
18
+ if (openedSheets > 0 && !scrollLockBackup) {
19
+ const scrollbarWidth = window.innerWidth - root.clientWidth;
20
+ scrollLockBackup = {
21
+ overflow: root.style.overflow,
22
+ paddingRight: root.style.paddingRight,
23
+ };
24
+ root.style.overflow = 'hidden';
25
+ if (scrollbarWidth > 0) {
26
+ // Avoids the layout shift caused by the scrollbar disappearing, in the page and in the fixed bar.
27
+ const paddingRight = parseFloat(getComputedStyle(root).paddingRight) || 0;
28
+ root.style.paddingRight = `${paddingRight + scrollbarWidth}px`;
29
+ root.style.setProperty('--dile-bottom-nav-scrollbar-width', `${scrollbarWidth}px`);
30
+ }
31
+ } else if (openedSheets === 0 && scrollLockBackup) {
32
+ root.style.overflow = scrollLockBackup.overflow;
33
+ root.style.paddingRight = scrollLockBackup.paddingRight;
34
+ root.style.removeProperty('--dile-bottom-nav-scrollbar-width');
35
+ scrollLockBackup = null;
36
+ }
37
+ }
38
+
39
+ // DileOverlay is only used for its updatePosition() method, which places the floating sheet
40
+ // next to the trigger and keeps it inside the viewport. open(), close() and toggle() are overridden.
41
+ export class DileBottomNavItem extends DileOverlay(DileCloseOnEscPressed(DileSelectableItem(LitElement))) {
42
+ static get properties() {
43
+ return {
44
+ /**
45
+ * Icon to display, in the dile-iconlib `"family.name"` format (e.g. "lucide.house").
46
+ */
47
+ icon: { type: String },
48
+ /**
49
+ * Text displayed under the icon.
50
+ */
51
+ label: { type: String },
52
+ /**
53
+ * Value used to select the item when the bar has attrForSelected="name".
54
+ */
55
+ name: { type: String, reflect: true },
56
+ /**
57
+ * If set, the item is rendered as a native link.
58
+ */
59
+ href: { type: String },
60
+ /**
61
+ * Target of the native link.
62
+ */
63
+ target: { type: String },
64
+ /**
65
+ * Opened status of the bottom sheet. Only applies when the item has slotted content.
66
+ */
67
+ opened: { type: Boolean, reflect: true },
68
+ /**
69
+ * If true, an item with a bottom sheet also becomes selected when it is clicked.
70
+ */
71
+ selectable: { type: Boolean },
72
+ /**
73
+ * If true, the bottom sheet closes when any link or button inside it is clicked.
74
+ */
75
+ closeOnClick: { type: Boolean, attribute: 'close-on-click' },
76
+ /**
77
+ * Icon displayed in the item while its bottom sheet is opened, in the dile-iconlib `"family.name"` format.
78
+ */
79
+ closeIcon: { type: String, attribute: 'close-icon' },
80
+ _hasSheet: { state: true },
81
+ _offset: { state: true },
82
+ _floating: { state: true },
83
+ };
84
+ }
85
+
86
+ constructor() {
87
+ super();
88
+ this.label = '';
89
+ this.opened = false;
90
+ this.selectable = false;
91
+ this.closeOnClick = false;
92
+ this._hasSheet = false;
93
+ this._offset = 0;
94
+ this._floating = false;
95
+ this.icon = '';
96
+ this.closeIcon = 'lucide.x';
97
+ // Floating sheet position, used when the bar is static (DileOverlay properties).
98
+ this.verticalAlign = 'top';
99
+ this.horizontalAlign = 'center';
100
+ this.moveTop = -8;
101
+ this._positionRafId = null;
102
+ this.viewportChangeHandler = this._onViewportChange.bind(this);
103
+ this.documentClickHandler = this._onDocumentClick.bind(this);
104
+ }
105
+
106
+ static get styles() {
107
+ return css`
108
+ * {
109
+ box-sizing: border-box;
110
+ }
111
+ :host {
112
+ display: block;
113
+ --dile-icon-size: var(--dile-bottom-nav-icon-size, 24px);
114
+ --dile-icon-color: var(--dile-bottom-nav-item-color, var(--dile-on-background-color, #666));
115
+ }
116
+ :host([selected]) {
117
+ --dile-icon-color: var(--dile-bottom-nav-item-selected-color, var(--dile-primary-color, #039be5));
118
+ }
119
+ .trigger {
120
+ all: unset;
121
+ box-sizing: border-box;
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: center;
125
+ justify-content: center;
126
+ gap: var(--dile-bottom-nav-item-gap, 2px);
127
+ width: 100%;
128
+ height: 100%;
129
+ padding: var(--dile-bottom-nav-item-padding, 4px 2px);
130
+ cursor: pointer;
131
+ color: var(--dile-bottom-nav-item-color, var(--dile-on-background-color, #666));
132
+ text-decoration: none;
133
+ -webkit-tap-highlight-color: transparent;
134
+ transition: color 0.2s ease;
135
+ }
136
+ :host([selected]) .trigger {
137
+ color: var(--dile-bottom-nav-item-selected-color, var(--dile-primary-color, #039be5));
138
+ }
139
+ .trigger:focus-visible {
140
+ outline: 2px solid var(--dile-bottom-nav-item-selected-color, var(--dile-primary-color, #039be5));
141
+ outline-offset: -2px;
142
+ border-radius: 4px;
143
+ }
144
+ .icon {
145
+ display: flex;
146
+ }
147
+ .label {
148
+ max-width: 100%;
149
+ font-size: var(--dile-bottom-nav-label-font-size, 0.75rem);
150
+ font-weight: var(--dile-bottom-nav-label-font-weight, normal);
151
+ white-space: nowrap;
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ }
155
+ :host([selected]) .label {
156
+ font-weight: var(--dile-bottom-nav-label-selected-font-weight, bold);
157
+ }
158
+
159
+ .close-icon {
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: center;
163
+ width: var(--dile-bottom-nav-close-icon-size, 44px);
164
+ height: var(--dile-bottom-nav-close-icon-size, 44px);
165
+ border-radius: 50%;
166
+ background-color: var(--dile-bottom-nav-close-icon-background-color, var(--dile-bottom-nav-item-selected-color, var(--dile-primary-color, #039be5)));
167
+ --dile-icon-color: var(--dile-bottom-nav-close-icon-color, var(--dile-bottom-nav-background-color, var(--dile-background-color, #fff)));
168
+ animation: dile-bottom-nav-close-in 0.3s ease;
169
+ }
170
+ @keyframes dile-bottom-nav-close-in {
171
+ from {
172
+ transform: rotate(-90deg) scale(0.5);
173
+ opacity: 0;
174
+ }
175
+ }
176
+ .visually-hidden {
177
+ position: absolute;
178
+ width: 1px;
179
+ height: 1px;
180
+ overflow: hidden;
181
+ clip: rect(0 0 0 0);
182
+ white-space: nowrap;
183
+ }
184
+
185
+ .overlay {
186
+ position: fixed;
187
+ top: 0;
188
+ left: 0;
189
+ right: 0;
190
+ z-index: 1;
191
+ background-color: var(--dile-bottom-nav-overlay-background-color, rgba(0, 0, 0, 0.5));
192
+ opacity: 0;
193
+ visibility: hidden;
194
+ transition: opacity 0.3s ease, visibility 0s linear 0.3s;
195
+ }
196
+ .sheet {
197
+ position: fixed;
198
+ left: var(--dile-bottom-nav-sheet-gap, 8px);
199
+ right: calc(var(--dile-bottom-nav-sheet-gap, 8px) + var(--dile-bottom-nav-scrollbar-width, 0px));
200
+ z-index: 2;
201
+ max-width: var(--dile-bottom-nav-sheet-max-width, none);
202
+ max-height: var(--dile-bottom-nav-sheet-max-height, 75vh);
203
+ margin: 0 auto;
204
+ overflow-y: auto;
205
+ overscroll-behavior: contain;
206
+ background-color: var(--dile-bottom-nav-sheet-background-color, var(--dile-background-color, #fff));
207
+ color: var(--dile-bottom-nav-sheet-text-color, var(--dile-on-background-color, #303030));
208
+ border-radius: var(--dile-bottom-nav-sheet-border-radius, 12px);
209
+ box-shadow: var(--dile-bottom-nav-sheet-box-shadow, 0 -4px 16px rgba(0, 0, 0, 0.2));
210
+ padding: var(--dile-bottom-nav-sheet-padding, 1rem);
211
+ opacity: 0;
212
+ transform: translateY(24px);
213
+ visibility: hidden;
214
+ transition: transform 0.3s ease, opacity 0.3s ease, visibility 0s linear 0.3s;
215
+ outline: none;
216
+ }
217
+ .sheet.floating {
218
+ right: auto;
219
+ width: var(--dile-bottom-nav-sheet-width, 280px);
220
+ max-width: calc(100vw - 25px);
221
+ margin: 0;
222
+ }
223
+ :host([opened]) .overlay {
224
+ opacity: 1;
225
+ visibility: visible;
226
+ transition: opacity 0.3s ease, visibility 0s;
227
+ }
228
+ :host([opened]) .sheet {
229
+ opacity: 1;
230
+ transform: translateY(0);
231
+ visibility: visible;
232
+ transition: transform 0.3s ease, opacity 0.3s ease, visibility 0s;
233
+ }
234
+ .nosheet, .overlay.floating {
235
+ display: none;
236
+ }
237
+
238
+ @media (prefers-reduced-motion: reduce) {
239
+ .trigger, .overlay, .sheet,
240
+ :host([opened]) .overlay, :host([opened]) .sheet {
241
+ transition: none;
242
+ }
243
+ .close-icon {
244
+ animation: none;
245
+ }
246
+ }
247
+ `;
248
+ }
249
+
250
+ // DileOverlay looks for #overlay and #trigger elements, not used in this component.
251
+ firstUpdated() {}
252
+
253
+ disconnectedCallback() {
254
+ super.disconnectedCallback();
255
+ this._removeOpenedListeners();
256
+ this._releaseScrollLock();
257
+ }
258
+
259
+ updated(changedProperties) {
260
+ if (changedProperties.has('opened')) {
261
+ const wasOpened = changedProperties.get('opened');
262
+ if (this.opened) {
263
+ this._onOpened();
264
+ } else if (wasOpened) {
265
+ this._releaseScrollLock();
266
+ this._removeOpenedListeners();
267
+ }
268
+ }
269
+ }
270
+
271
+ _onOpened() {
272
+ if (this._floating) {
273
+ this._positionFloating();
274
+ document.addEventListener('click', this.documentClickHandler);
275
+ window.addEventListener('scroll', this.viewportChangeHandler, { passive: true, capture: true });
276
+ } else {
277
+ const sheet = this.shadowRoot.querySelector('.sheet');
278
+ sheet.style.top = '';
279
+ sheet.style.left = '';
280
+ if (!this._lockingScroll) {
281
+ this._lockingScroll = true;
282
+ openedSheets++;
283
+ updateScrollLock();
284
+ }
285
+ this._computeOffset();
286
+ }
287
+ window.addEventListener('resize', this.viewportChangeHandler, { passive: true });
288
+ }
289
+
290
+ _removeOpenedListeners() {
291
+ document.removeEventListener('click', this.documentClickHandler);
292
+ window.removeEventListener('scroll', this.viewportChangeHandler, { capture: true });
293
+ window.removeEventListener('resize', this.viewportChangeHandler);
294
+ if (this._positionRafId) {
295
+ cancelAnimationFrame(this._positionRafId);
296
+ this._positionRafId = null;
297
+ }
298
+ }
299
+
300
+ _onViewportChange() {
301
+ if (this._positionRafId) return;
302
+ this._positionRafId = requestAnimationFrame(() => {
303
+ this._positionRafId = null;
304
+ if (!this.opened) return;
305
+ if (this._floating) {
306
+ this._positionFloating();
307
+ } else {
308
+ this._computeOffset();
309
+ }
310
+ });
311
+ }
312
+
313
+ // Places the floating sheet over the trigger, centered and inside the viewport.
314
+ _positionFloating() {
315
+ this.overlay = this.shadowRoot.querySelector('.sheet');
316
+ this.trigger = this.shadowRoot.querySelector('.trigger');
317
+ this.updatePosition();
318
+ }
319
+
320
+ _onDocumentClick(e) {
321
+ if (!e.composedPath().includes(this)) {
322
+ this.close();
323
+ }
324
+ }
325
+
326
+ // Distance from the top of the bar to the bottom of the viewport, so the sheet and the overlay do not cover the bar.
327
+ _computeOffset() {
328
+ const bar = this.closest('dile-bottom-nav') || this;
329
+ const offset = window.innerHeight - bar.getBoundingClientRect().top;
330
+ this._offset = Math.min(Math.max(offset, 0), window.innerHeight);
331
+ }
332
+
333
+ _releaseScrollLock() {
334
+ if (this._lockingScroll) {
335
+ this._lockingScroll = false;
336
+ openedSheets--;
337
+ updateScrollLock();
338
+ }
339
+ }
340
+
341
+ render() {
342
+ return html`
343
+ ${this.href && !this._hasSheet ? this.linkTemplate : this.buttonTemplate}
344
+ <div
345
+ class="overlay ${this._hasSheet ? '' : 'nosheet'} ${this._floating ? 'floating' : ''}"
346
+ style="bottom: ${this._offset}px"
347
+ @click="${this.close}"
348
+ ></div>
349
+ <div
350
+ class="sheet ${this._hasSheet ? '' : 'nosheet'} ${this._floating ? 'floating' : ''}"
351
+ style=${styleMap({ bottom: this._floating ? 'auto' : `calc(${this._offset}px + var(--dile-bottom-nav-sheet-gap, 8px))` })}
352
+ role="dialog"
353
+ aria-modal="${!this._floating}"
354
+ aria-label="${this.label}"
355
+ aria-hidden="${!this.opened}"
356
+ tabindex="-1"
357
+ ?inert="${!this.opened}"
358
+ >
359
+ <slot @slotchange="${this._onSlotChange}" @click="${this._onSheetClick}"></slot>
360
+ </div>
361
+ `;
362
+ }
363
+
364
+ get contentTemplate() {
365
+ if (this.opened && this._hasSheet) {
366
+ return html`
367
+ <span class="close-icon" aria-hidden="true"><dile-iconlib icon=${this.closeIcon}></dile-iconlib></span>
368
+ <span class="visually-hidden">${this.label}</span>
369
+ `;
370
+ }
371
+ return html`
372
+ <span class="icon" aria-hidden="true">
373
+ <slot name="icon">${this.icon ? html`<dile-iconlib icon=${this.icon}></dile-iconlib>` : ''}</slot>
374
+ </span>
375
+ <span class="label">${this.label}</span>
376
+ `;
377
+ }
378
+
379
+ get linkTemplate() {
380
+ return html`
381
+ <a
382
+ class="trigger"
383
+ href="${this.href}"
384
+ target="${ifDefined(this.target)}"
385
+ aria-current="${ifDefined(this.selected ? 'page' : undefined)}"
386
+ @click="${this._onTriggerClick}"
387
+ >${this.contentTemplate}</a>
388
+ `;
389
+ }
390
+
391
+ get buttonTemplate() {
392
+ return html`
393
+ <button
394
+ class="trigger"
395
+ type="button"
396
+ aria-current="${ifDefined(this.selected && !this._hasSheet ? 'page' : undefined)}"
397
+ aria-haspopup="${ifDefined(this._hasSheet ? 'dialog' : undefined)}"
398
+ aria-expanded="${ifDefined(this._hasSheet ? String(this.opened) : undefined)}"
399
+ @click="${this._onTriggerClick}"
400
+ >${this.contentTemplate}</button>
401
+ `;
402
+ }
403
+
404
+ _onSlotChange(e) {
405
+ this._hasSheet = e.target.assignedNodes({ flatten: true }).some(node =>
406
+ node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '')
407
+ );
408
+ if (!this._hasSheet && this.opened) {
409
+ this.close();
410
+ }
411
+ }
412
+
413
+ _onTriggerClick() {
414
+ if (this._hasSheet) {
415
+ if (this.selectable) {
416
+ this.select();
417
+ }
418
+ this.toggle();
419
+ } else {
420
+ this.select();
421
+ }
422
+ this.dispatchEvent(new CustomEvent('dile-bottom-nav-item-click', {
423
+ bubbles: true,
424
+ composed: true,
425
+ detail: { name: this.name, item: this },
426
+ }));
427
+ }
428
+
429
+ _onSheetClick(e) {
430
+ const path = e.composedPath();
431
+ const limit = path.indexOf(e.currentTarget);
432
+ const nodes = limit >= 0 ? path.slice(0, limit) : path;
433
+ const closer = nodes.find(node =>
434
+ node instanceof Element &&
435
+ (node.hasAttribute('data-close') || (this.closeOnClick && (node.tagName === 'A' || node.tagName === 'BUTTON')))
436
+ );
437
+ if (closer) {
438
+ this.close();
439
+ }
440
+ }
441
+
442
+ toggle() {
443
+ if (this.opened) {
444
+ this.close();
445
+ } else {
446
+ this.open();
447
+ }
448
+ }
449
+
450
+ async open() {
451
+ if (this.opened || !this._hasSheet) {
452
+ return;
453
+ }
454
+ // When the bar is static the sheet is a floating card over the item, without overlay.
455
+ this._floating = !!this.closest('dile-bottom-nav')?.static;
456
+ this.opened = true;
457
+ this.dispatchEvent(new CustomEvent('dile-bottom-nav-sheet-opened', {
458
+ bubbles: true,
459
+ composed: true,
460
+ detail: { name: this.name, item: this },
461
+ }));
462
+ await this.updateComplete;
463
+ this.shadowRoot.querySelector('.sheet').focus({ preventScroll: true });
464
+ }
465
+
466
+ async close() {
467
+ if (!this.opened) {
468
+ return;
469
+ }
470
+ const hadFocus = this.matches(':focus-within');
471
+ this.opened = false;
472
+ this.dispatchEvent(new CustomEvent('dile-bottom-nav-sheet-closed', {
473
+ bubbles: true,
474
+ composed: true,
475
+ detail: { name: this.name, item: this },
476
+ }));
477
+ await this.updateComplete;
478
+ if (hadFocus) {
479
+ this.shadowRoot.querySelector('.trigger').focus({ preventScroll: true });
480
+ }
481
+ }
482
+ }