@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,5 @@
1
+ import { DileBottomNavItem } from "./src/DileBottomNavItem.js";
2
+
3
+ if (!customElements.get("dile-bottom-nav-item")) {
4
+ window.customElements.define("dile-bottom-nav-item", DileBottomNavItem);
5
+ }
@@ -0,0 +1,6 @@
1
+ import './bottom-nav-item.js';
2
+ import { DileBottomNav } from "./src/DileBottomNav.js";
3
+
4
+ if (!customElements.get("dile-bottom-nav")) {
5
+ window.customElements.define("dile-bottom-nav", DileBottomNav);
6
+ }
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import '@dile/iconlib/lucide-icons/house.js';
3
+ import './bottom-nav.js';
4
+
5
+ describe('dile-bottom-nav', () => {
6
+ afterEach(() => {
7
+ document.body.innerHTML = '';
8
+ document.documentElement.style.overflow = '';
9
+ });
10
+
11
+ async function renderNav(html) {
12
+ document.body.innerHTML = html;
13
+ const el = document.body.querySelector('dile-bottom-nav');
14
+ await el.updateComplete;
15
+ const items = [...el.querySelectorAll('dile-bottom-nav-item')];
16
+ await Promise.all(items.map((item) => item.updateComplete));
17
+ // Wait for slotchange to detect the sheets.
18
+ await new Promise((resolve) => setTimeout(resolve));
19
+ await Promise.all(items.map((item) => item.updateComplete));
20
+ return { el, items };
21
+ }
22
+
23
+ function trigger(item) {
24
+ return item.shadowRoot.querySelector('.trigger');
25
+ }
26
+
27
+ it('renders a navigation landmark with the slotted items', async () => {
28
+ const { el, items } = await renderNav(`
29
+ <dile-bottom-nav label="App navigation">
30
+ <dile-bottom-nav-item label="Home"></dile-bottom-nav-item>
31
+ <dile-bottom-nav-item label="Search"></dile-bottom-nav-item>
32
+ </dile-bottom-nav>
33
+ `);
34
+
35
+ const nav = el.shadowRoot.querySelector('nav');
36
+ expect(nav.getAttribute('aria-label')).toBe('App navigation');
37
+ expect(items.length).toBe(2);
38
+ expect(items[0].shadowRoot.querySelector('.label').textContent).toBe('Home');
39
+ });
40
+
41
+ it('is fixed by default and in the document flow when static', async () => {
42
+ const { el } = await renderNav(`<dile-bottom-nav></dile-bottom-nav>`);
43
+ expect(getComputedStyle(el).position).toBe('fixed');
44
+
45
+ el.static = true;
46
+ await el.updateComplete;
47
+ expect(getComputedStyle(el).position).toBe('relative');
48
+ });
49
+
50
+ it('compensates the scrollbar width in the fixed bar while the page scroll is locked', async () => {
51
+ const { el } = await renderNav(`<dile-bottom-nav></dile-bottom-nav>`);
52
+ document.documentElement.style.setProperty('--dile-bottom-nav-scrollbar-width', '15px');
53
+
54
+ expect(getComputedStyle(el).paddingRight).toBe('15px');
55
+ el.static = true;
56
+ await el.updateComplete;
57
+ expect(getComputedStyle(el).paddingRight).toBe('0px');
58
+
59
+ document.documentElement.style.removeProperty('--dile-bottom-nav-scrollbar-width');
60
+ });
61
+
62
+ it('renders the icon of the item from a dile-iconlib string', async () => {
63
+ const { items } = await renderNav(`
64
+ <dile-bottom-nav>
65
+ <dile-bottom-nav-item label="Home" icon="lucide.house"></dile-bottom-nav-item>
66
+ <dile-bottom-nav-item label="Search"></dile-bottom-nav-item>
67
+ </dile-bottom-nav>
68
+ `);
69
+
70
+ const icon = items[0].shadowRoot.querySelector('dile-iconlib');
71
+ expect(icon.getAttribute('icon')).toBe('lucide.house');
72
+ expect(items[1].shadowRoot.querySelector('dile-iconlib')).toBeNull();
73
+ });
74
+
75
+ it('marks the selected item by attribute and changes selection on click', async () => {
76
+ const { el, items } = await renderNav(`
77
+ <dile-bottom-nav attrForSelected="name" selected="home">
78
+ <dile-bottom-nav-item name="home" label="Home"></dile-bottom-nav-item>
79
+ <dile-bottom-nav-item name="search" label="Search"></dile-bottom-nav-item>
80
+ </dile-bottom-nav>
81
+ `);
82
+
83
+ expect(items[0].selected).toBe(true);
84
+ expect(trigger(items[0]).getAttribute('aria-current')).toBe('page');
85
+
86
+ let detail;
87
+ el.addEventListener('dile-selected-changed', (e) => { detail = e.detail; });
88
+ trigger(items[1]).click();
89
+ await el.updateComplete;
90
+
91
+ expect(el.selected).toBe('search');
92
+ expect(detail.selected).toBe('search');
93
+ expect(items[1].selected).toBe(true);
94
+ expect(items[0].selected).toBe(false);
95
+ });
96
+
97
+ it('dispatches dile-bottom-nav-item-click with the item name', async () => {
98
+ const { el, items } = await renderNav(`
99
+ <dile-bottom-nav>
100
+ <dile-bottom-nav-item name="home" label="Home"></dile-bottom-nav-item>
101
+ </dile-bottom-nav>
102
+ `);
103
+
104
+ let detail;
105
+ el.addEventListener('dile-bottom-nav-item-click', (e) => { detail = e.detail; });
106
+ trigger(items[0]).click();
107
+
108
+ expect(detail.name).toBe('home');
109
+ });
110
+
111
+ it('renders a native link when the item has an href', async () => {
112
+ const { items } = await renderNav(`
113
+ <dile-bottom-nav>
114
+ <dile-bottom-nav-item label="Home" href="#home" target="_self"></dile-bottom-nav-item>
115
+ <dile-bottom-nav-item label="Search"></dile-bottom-nav-item>
116
+ </dile-bottom-nav>
117
+ `);
118
+
119
+ const link = items[0].shadowRoot.querySelector('a');
120
+ expect(link.getAttribute('href')).toBe('#home');
121
+ expect(link.getAttribute('target')).toBe('_self');
122
+ expect(items[1].shadowRoot.querySelector('a')).toBeNull();
123
+ });
124
+
125
+ it('opens the bottom sheet of an item with slotted content without changing the selection', async () => {
126
+ const { el, items } = await renderNav(`
127
+ <dile-bottom-nav attrForSelected="name" selected="home">
128
+ <dile-bottom-nav-item name="home" label="Home"></dile-bottom-nav-item>
129
+ <dile-bottom-nav-item name="create" label="Create">
130
+ <a href="#new">New</a>
131
+ </dile-bottom-nav-item>
132
+ </dile-bottom-nav>
133
+ `);
134
+
135
+ const button = trigger(items[1]);
136
+ expect(button.getAttribute('aria-haspopup')).toBe('dialog');
137
+ expect(button.getAttribute('aria-expanded')).toBe('false');
138
+
139
+ let opened = false;
140
+ el.addEventListener('dile-bottom-nav-sheet-opened', () => { opened = true; });
141
+ button.click();
142
+ await items[1].updateComplete;
143
+
144
+ expect(items[1].opened).toBe(true);
145
+ expect(opened).toBe(true);
146
+ expect(button.getAttribute('aria-expanded')).toBe('true');
147
+ expect(document.documentElement.style.overflow).toBe('hidden');
148
+ expect(el.selected).toBe('home');
149
+ expect(items[0].selected).toBe(true);
150
+ });
151
+
152
+ it('closes the sheet when clicking the overlay', async () => {
153
+ const { items } = await renderNav(`
154
+ <dile-bottom-nav>
155
+ <dile-bottom-nav-item label="Create"><p>Content</p></dile-bottom-nav-item>
156
+ </dile-bottom-nav>
157
+ `);
158
+
159
+ await items[0].open();
160
+ let closed = false;
161
+ items[0].addEventListener('dile-bottom-nav-sheet-closed', () => { closed = true; });
162
+ items[0].shadowRoot.querySelector('.overlay').click();
163
+ await items[0].updateComplete;
164
+
165
+ expect(items[0].opened).toBe(false);
166
+ expect(closed).toBe(true);
167
+ expect(document.documentElement.style.overflow).toBe('');
168
+ });
169
+
170
+ it('closes the sheet when pressing Escape', async () => {
171
+ const { items } = await renderNav(`
172
+ <dile-bottom-nav>
173
+ <dile-bottom-nav-item label="Create"><p>Content</p></dile-bottom-nav-item>
174
+ </dile-bottom-nav>
175
+ `);
176
+
177
+ await items[0].open();
178
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
179
+ await items[0].updateComplete;
180
+
181
+ expect(items[0].opened).toBe(false);
182
+ });
183
+
184
+ it('closes the sheet when clicking an element with data-close', async () => {
185
+ const { items } = await renderNav(`
186
+ <dile-bottom-nav>
187
+ <dile-bottom-nav-item label="Create">
188
+ <button id="keep">Keep</button>
189
+ <button id="close" data-close>Close</button>
190
+ </dile-bottom-nav-item>
191
+ </dile-bottom-nav>
192
+ `);
193
+
194
+ await items[0].open();
195
+ document.getElementById('keep').click();
196
+ await items[0].updateComplete;
197
+ expect(items[0].opened).toBe(true);
198
+
199
+ document.getElementById('close').click();
200
+ await items[0].updateComplete;
201
+ expect(items[0].opened).toBe(false);
202
+ });
203
+
204
+ it('closes the sheet when clicking any link or button with close-on-click', async () => {
205
+ const { items } = await renderNav(`
206
+ <dile-bottom-nav>
207
+ <dile-bottom-nav-item label="Create" close-on-click>
208
+ <button id="action">Action</button>
209
+ </dile-bottom-nav-item>
210
+ </dile-bottom-nav>
211
+ `);
212
+
213
+ await items[0].open();
214
+ document.getElementById('action').click();
215
+ await items[0].updateComplete;
216
+
217
+ expect(items[0].opened).toBe(false);
218
+ });
219
+
220
+ it('keeps only one sheet opened at a time', async () => {
221
+ const { items } = await renderNav(`
222
+ <dile-bottom-nav>
223
+ <dile-bottom-nav-item label="One"><p>One</p></dile-bottom-nav-item>
224
+ <dile-bottom-nav-item label="Two"><p>Two</p></dile-bottom-nav-item>
225
+ </dile-bottom-nav>
226
+ `);
227
+
228
+ await items[0].open();
229
+ await items[1].open();
230
+ await items[0].updateComplete;
231
+
232
+ expect(items[0].opened).toBe(false);
233
+ expect(items[1].opened).toBe(true);
234
+ });
235
+
236
+ it('replaces the item icon with the close icon while the sheet is opened', async () => {
237
+ const { items } = await renderNav(`
238
+ <dile-bottom-nav>
239
+ <dile-bottom-nav-item label="Create"><p>Content</p></dile-bottom-nav-item>
240
+ </dile-bottom-nav>
241
+ `);
242
+
243
+ expect(items[0].shadowRoot.querySelector('.close-icon')).toBeNull();
244
+ await items[0].open();
245
+ expect(items[0].shadowRoot.querySelector('.close-icon dile-iconlib').getAttribute('icon')).toBe('lucide.x');
246
+ expect(items[0].shadowRoot.querySelector('.trigger').textContent).toContain('Create');
247
+
248
+ trigger(items[0]).click();
249
+ await items[0].updateComplete;
250
+ expect(items[0].opened).toBe(false);
251
+ expect(items[0].shadowRoot.querySelector('.close-icon')).toBeNull();
252
+ });
253
+
254
+ it('places the sheet and the overlay above the bar', async () => {
255
+ const { el, items } = await renderNav(`
256
+ <dile-bottom-nav>
257
+ <dile-bottom-nav-item label="Create"><p>Content</p></dile-bottom-nav-item>
258
+ </dile-bottom-nav>
259
+ `);
260
+
261
+ await items[0].open();
262
+ await items[0].updateComplete;
263
+ const barTop = el.getBoundingClientRect().top;
264
+ const barHeight = window.innerHeight - barTop;
265
+ // Read the computed bottom instead of the bounding box, which is affected by the opening transition.
266
+ const sheetBottom = parseFloat(getComputedStyle(items[0].shadowRoot.querySelector('.sheet')).bottom);
267
+ const overlay = items[0].shadowRoot.querySelector('.overlay').getBoundingClientRect();
268
+
269
+ expect(sheetBottom).toBeGreaterThanOrEqual(Math.floor(barHeight));
270
+ expect(Math.round(overlay.bottom)).toBe(Math.round(barTop));
271
+ });
272
+
273
+ describe('static bar', () => {
274
+ async function renderStatic() {
275
+ const result = await renderNav(`
276
+ <div style="height: 400px"></div>
277
+ <dile-bottom-nav static style="width: 300px; margin: 0 auto;">
278
+ <dile-bottom-nav-item label="Home"></dile-bottom-nav-item>
279
+ <dile-bottom-nav-item label="Create"><button id="inside">Inside</button></dile-bottom-nav-item>
280
+ </dile-bottom-nav>
281
+ <button id="outside">Outside</button>
282
+ `);
283
+ await result.items[1].open();
284
+ await result.items[1].updateComplete;
285
+ return result;
286
+ }
287
+
288
+ it('opens a floating sheet without overlay and without locking the page scroll', async () => {
289
+ const { items } = await renderStatic();
290
+ const overlay = items[1].shadowRoot.querySelector('.overlay');
291
+ const sheet = items[1].shadowRoot.querySelector('.sheet');
292
+
293
+ expect(getComputedStyle(overlay).display).toBe('none');
294
+ expect(sheet.classList.contains('floating')).toBe(true);
295
+ expect(sheet.getAttribute('aria-modal')).toBe('false');
296
+ expect(document.documentElement.style.overflow).toBe('');
297
+ });
298
+
299
+ it('places the floating sheet above the trigger, centered and inside the viewport', async () => {
300
+ const { items } = await renderStatic();
301
+ const sheet = items[1].shadowRoot.querySelector('.sheet');
302
+ const triggerRect = trigger(items[1]).getBoundingClientRect();
303
+ // Inline position set by DileOverlay, not affected by the opening transition.
304
+ const top = parseFloat(sheet.style.top);
305
+ const left = parseFloat(sheet.style.left);
306
+ const width = sheet.offsetWidth;
307
+
308
+ expect(top + sheet.offsetHeight).toBeLessThanOrEqual(triggerRect.top);
309
+ expect(left).toBeGreaterThanOrEqual(10);
310
+ expect(left + width).toBeLessThanOrEqual(document.documentElement.clientWidth - 10);
311
+ const centered = triggerRect.left + triggerRect.width / 2 - width / 2;
312
+ const clamped = Math.min(Math.max(centered, 10), document.documentElement.clientWidth - width - 15);
313
+ expect(Math.round(left)).toBe(Math.round(clamped));
314
+ });
315
+
316
+ it('closes the floating sheet when clicking outside, but not inside', async () => {
317
+ const { items } = await renderStatic();
318
+
319
+ document.getElementById('inside').click();
320
+ await items[1].updateComplete;
321
+ expect(items[1].opened).toBe(true);
322
+
323
+ document.getElementById('outside').click();
324
+ await items[1].updateComplete;
325
+ expect(items[1].opened).toBe(false);
326
+ });
327
+
328
+ it('toggles the floating sheet from the trigger', async () => {
329
+ const { items } = await renderStatic();
330
+
331
+ trigger(items[1]).click();
332
+ await items[1].updateComplete;
333
+ expect(items[1].opened).toBe(false);
334
+
335
+ trigger(items[1]).click();
336
+ await items[1].updateComplete;
337
+ expect(items[1].opened).toBe(true);
338
+ });
339
+ });
340
+ });
@@ -0,0 +1,2 @@
1
+ export { DileBottomNav } from './src/DileBottomNav.js';
2
+ export { DileBottomNavItem } from './src/DileBottomNavItem.js';
@@ -0,0 +1,88 @@
1
+ import { html, css, LitElement } from "lit";
2
+ import { DileSelectable } from "../../../mixins/selectable/index.js";
3
+
4
+ export class DileBottomNav extends DileSelectable(LitElement) {
5
+ static get properties() {
6
+ return {
7
+ /**
8
+ * If true the bar is not fixed at the bottom of the viewport, it is rendered in the document flow.
9
+ */
10
+ static: { type: Boolean, reflect: true },
11
+ /**
12
+ * Accessible name of the navigation landmark.
13
+ */
14
+ label: { type: String },
15
+ };
16
+ }
17
+
18
+ constructor() {
19
+ super();
20
+ this.static = false;
21
+ this.label = 'Main navigation';
22
+ this.sheetOpenedHandler = this._closeOtherSheets.bind(this);
23
+ }
24
+
25
+ static get styles() {
26
+ return css`
27
+ * {
28
+ box-sizing: border-box;
29
+ }
30
+ :host {
31
+ display: block;
32
+ position: fixed;
33
+ left: 0;
34
+ right: 0;
35
+ bottom: 0;
36
+ z-index: var(--dile-bottom-nav-z-index, 100);
37
+ background-color: var(--dile-bottom-nav-background-color, var(--dile-background-color, #fff));
38
+ border-top: var(--dile-bottom-nav-border-top, 1px solid rgba(0, 0, 0, 0.1));
39
+ box-shadow: var(--dile-bottom-nav-box-shadow, 0 -2px 8px rgba(0, 0, 0, 0.08));
40
+ padding-bottom: env(safe-area-inset-bottom, 0);
41
+ }
42
+ :host(:not([static])) {
43
+ /* Set while a bottom sheet locks the page scroll, so the items don't move when the scrollbar disappears. */
44
+ padding-right: var(--dile-bottom-nav-scrollbar-width, 0px);
45
+ }
46
+ :host([static]) {
47
+ position: relative;
48
+ }
49
+ nav {
50
+ display: flex;
51
+ align-items: stretch;
52
+ height: var(--dile-bottom-nav-height, 56px);
53
+ max-width: var(--dile-bottom-nav-max-width, none);
54
+ margin: 0 auto;
55
+ }
56
+ ::slotted(*) {
57
+ flex: 1 1 0;
58
+ min-width: 0;
59
+ }
60
+ `;
61
+ }
62
+
63
+ connectedCallback() {
64
+ super.connectedCallback();
65
+ this.addEventListener('dile-bottom-nav-sheet-opened', this.sheetOpenedHandler);
66
+ }
67
+
68
+ disconnectedCallback() {
69
+ super.disconnectedCallback();
70
+ this.removeEventListener('dile-bottom-nav-sheet-opened', this.sheetOpenedHandler);
71
+ }
72
+
73
+ render() {
74
+ return html`
75
+ <nav aria-label="${this.label}">
76
+ <slot></slot>
77
+ </nav>
78
+ `;
79
+ }
80
+
81
+ _closeOtherSheets(e) {
82
+ this.getItems().forEach(item => {
83
+ if (item !== e.detail.item && item.opened && typeof item.close === 'function') {
84
+ item.close();
85
+ }
86
+ });
87
+ }
88
+ }