Skip to content

Commit 5583b5d

Browse files
authored
fix: slot dashboard widgets instead of dom reordering (#8359)
1 parent 4348a02 commit 5583b5d

File tree

5 files changed

+95
-21
lines changed

5 files changed

+95
-21
lines changed

packages/dashboard/src/vaadin-dashboard-section.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
173173
type: String,
174174
value: '',
175175
},
176+
177+
/** @private */
178+
__childCount: {
179+
type: Number,
180+
value: 0,
181+
},
176182
};
177183
}
178184

@@ -191,7 +197,10 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
191197
</header>
192198
</div>
193199
200+
<!-- Default slot is used by <vaadin-dashboard-layout> -->
194201
<slot></slot>
202+
<!-- Named slots are used by <vaadin-dashboard> -->
203+
${[...Array(this.__childCount)].map((_, index) => html`<slot name="slot-${index}"></slot>`)}
195204
`;
196205
}
197206

packages/dashboard/src/vaadin-dashboard.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitM
195195
};
196196
},
197197
},
198+
199+
/** @private */
200+
__childCount: {
201+
type: Number,
202+
value: 0,
203+
},
198204
};
199205
}
200206

@@ -221,11 +227,14 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitM
221227

222228
/** @protected */
223229
render() {
224-
return html`<div id="grid"><slot></slot></div>`;
230+
return html`<div id="grid">
231+
${[...Array(this.__childCount)].map((_, index) => html`<slot name="slot-${index}"></slot>`)}
232+
</div>`;
225233
}
226234

227235
/** @private */
228236
__itemsOrRendererChanged(items, renderer) {
237+
this.__childCount = items ? items.length : 0;
229238
this.__renderItemWrappers(items || []);
230239

231240
this.querySelectorAll(WRAPPER_LOCAL_NAME).forEach((wrapper) => {
@@ -257,38 +266,25 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitM
257266
__renderItemWrappers(items, hostElement = this) {
258267
// Get all the wrappers in the host element
259268
let wrappers = [...hostElement.children].filter((el) => el.localName === WRAPPER_LOCAL_NAME);
260-
let previousWrapper = null;
261269

262270
const focusedWrapper = wrappers.find((wrapper) => wrapper.querySelector(':focus'));
263271
const focusedWrapperWillBeRemoved = focusedWrapper && !this.__isActiveWrapper(focusedWrapper);
264272
const wrapperClosestToRemovedFocused =
265273
focusedWrapperWillBeRemoved && this.__getClosestActiveWrapper(focusedWrapper);
266274

267-
items.forEach((item) => {
275+
items.forEach((item, index) => {
268276
// Find the wrapper for the item or create a new one
269277
const wrapper = wrappers.find((el) => itemsEqual(getElementItem(el), item)) || this.__createWrapper(item);
270278
wrappers = wrappers.filter((el) => el !== wrapper);
279+
if (!wrapper.isConnected) {
280+
hostElement.appendChild(wrapper);
281+
}
271282

272283
// Update the wrapper style
273284
this.__updateWrapper(wrapper, item);
274285

275-
if (wrapper !== focusedWrapper) {
276-
if (previousWrapper) {
277-
// Append the wrapper after the previous one if it's not already there
278-
if (wrapper.previousElementSibling !== previousWrapper) {
279-
previousWrapper.after(wrapper);
280-
}
281-
} else if (hostElement.firstChild) {
282-
// Insert the wrapper as the first child of the host element if it's not already there
283-
if (wrapper !== hostElement.firstChild) {
284-
hostElement.insertBefore(wrapper, hostElement.firstChild);
285-
}
286-
} else {
287-
// Append the wrapper to the empty host element
288-
hostElement.appendChild(wrapper);
289-
}
290-
}
291-
previousWrapper = wrapper;
286+
// Update the wrapper slot
287+
wrapper.slot = `slot-${index}`;
292288

293289
// Render section if the item has subitems
294290
if (item.items) {
@@ -307,6 +303,7 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitM
307303
section.__i18n = this.i18n;
308304

309305
// Render the subitems
306+
section.__childCount = item.items.length;
310307
this.__renderItemWrappers(item.items, section);
311308
}
312309
});
@@ -368,20 +365,35 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitM
368365
return getItemsArrayOfItem(getElementItem(wrapper), this.items);
369366
}
370367

368+
/**
369+
* Parses the slot name to get the index of the item in the dashboard
370+
* For example, slot name "slot-12" will return 12
371+
* @private
372+
*/
373+
__parseSlotIndex(slotName) {
374+
return parseInt(slotName.split('-')[1]);
375+
}
376+
371377
/** @private */
372378
__getClosestActiveWrapper(wrapper) {
373379
if (!wrapper || this.__isActiveWrapper(wrapper)) {
374380
return wrapper;
375381
}
376382

383+
// Sibling wrappers sorted by their slot name
384+
const siblingWrappers = [...wrapper.parentElement.children].sort((a, b) => {
385+
return this.__parseSlotIndex(a.slot) - this.__parseSlotIndex(b.slot);
386+
});
387+
377388
// Starting from the given wrapper element, iterates through the siblings in the given direction
378389
// to find the closest wrapper that represents an item in the dashboard's items array
379390
const findSiblingWrapper = (wrapper, dir) => {
380391
while (wrapper) {
381392
if (this.__isActiveWrapper(wrapper)) {
382393
return wrapper;
383394
}
384-
wrapper = dir === 1 ? wrapper.nextElementSibling : wrapper.previousElementSibling;
395+
const currentIndex = siblingWrappers.indexOf(wrapper);
396+
wrapper = dir === 1 ? siblingWrappers[currentIndex + 1] : siblingWrappers[currentIndex - 1];
385397
}
386398
};
387399

packages/dashboard/test/dashboard-keyboard.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
setMinimumColumnWidth,
2424
setMinimumRowHeight,
2525
setSpacing,
26+
updateComplete,
2627
} from './helpers.js';
2728

2829
type TestDashboardItem = DashboardItem & { id: number };
@@ -209,6 +210,7 @@ describe('dashboard - keyboard interaction', () => {
209210

210211
it('should move the widget backwards on arrow up', async () => {
211212
await sendKeys({ press: 'ArrowDown' });
213+
await updateComplete(dashboard);
212214
await sendKeys({ press: 'ArrowUp' });
213215
expect(dashboard.items).to.eql([{ id: 0 }, { id: 1 }, { items: [{ id: 2 }, { id: 3 }] }]);
214216
});
@@ -245,6 +247,7 @@ describe('dashboard - keyboard interaction', () => {
245247
setMinimumRowHeight(dashboard, 100);
246248
await sendKeys({ down: 'Shift' });
247249
await sendKeys({ press: 'ArrowDown' });
250+
await updateComplete(dashboard);
248251
await sendKeys({ press: 'ArrowUp' });
249252
await sendKeys({ up: 'Shift' });
250253
expect((dashboard.items[0] as DashboardItem).rowspan).to.equal(1);
@@ -363,6 +366,7 @@ describe('dashboard - keyboard interaction', () => {
363366

364367
it('should move the widget backwards on arrow backwards', async () => {
365368
await sendKeys({ press: arrowForwards });
369+
await updateComplete(dashboard);
366370
await sendKeys({ press: arrowBackwards });
367371
expect(dashboard.items).to.eql([{ id: 0 }, { id: 1 }, { items: [{ id: 2 }, { id: 3 }] }]);
368372
});
@@ -377,6 +381,7 @@ describe('dashboard - keyboard interaction', () => {
377381
it('should decrease the widget column span on shift + arrow backwards', async () => {
378382
await sendKeys({ down: 'Shift' });
379383
await sendKeys({ press: arrowForwards });
384+
await updateComplete(dashboard);
380385
await sendKeys({ press: arrowBackwards });
381386
await sendKeys({ up: 'Shift' });
382387
expect((dashboard.items[0] as DashboardItem).colspan).to.equal(1);

packages/dashboard/test/dashboard-widget-reordering.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
setMinimumColumnWidth,
2222
setMinimumRowHeight,
2323
setSpacing,
24+
updateComplete,
2425
} from './helpers.js';
2526

2627
type TestDashboardItem = DashboardItem & { id: number };
@@ -613,4 +614,35 @@ describe('dashboard - widget reordering', () => {
613614
});
614615
});
615616
});
617+
618+
it('should not disconnect widgets when reordering', async () => {
619+
// Define a test element that spies on disconnectedCallback
620+
const disconnectSpy = sinon.spy();
621+
customElements.define(
622+
'disconnect-test-element',
623+
class extends HTMLElement {
624+
disconnectedCallback() {
625+
disconnectSpy();
626+
}
627+
},
628+
);
629+
630+
// Assign a renderer that uses the test element
631+
dashboard.renderer = (root, _, model) => {
632+
if (root.querySelector('disconnect-test-element')) {
633+
return;
634+
}
635+
root.innerHTML = `<vaadin-dashboard-widget id="${model.item.id}" widget-title="${model.item.id} title">
636+
<disconnect-test-element></disconnect-test-element>
637+
</vaadin-dashboard-widget>`;
638+
};
639+
await updateComplete(dashboard);
640+
641+
// Reorder the items
642+
dashboard.items = [dashboard.items[1], dashboard.items[0]];
643+
await updateComplete(dashboard);
644+
645+
// Expect no test element to have been disconnected
646+
expect(disconnectSpy).to.not.have.been.called;
647+
});
616648
});

packages/dashboard/test/dashboard.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,22 @@ describe('dashboard', () => {
681681
expect(document.activeElement).to.equal(getElementFromCell(dashboard, 0, 0)!);
682682
});
683683

684+
it('should focus next widget on focused widget removal after reordering', async () => {
685+
getElementFromCell(dashboard, 0, 0)!.focus();
686+
// Reorder items (the focused widget is moved to the end)
687+
dashboard.items = [dashboard.items[1], dashboard.items[2], dashboard.items[0]];
688+
await updateComplete(dashboard);
689+
690+
// Remove the focused widget
691+
dashboard.items = [dashboard.items[0], dashboard.items[1]];
692+
await updateComplete(dashboard);
693+
694+
// Expect the section to be focused
695+
const sectionWidget = getElementFromCell(dashboard, 1, 0)!;
696+
const section = getParentSection(sectionWidget)!;
697+
expect(document.activeElement).to.equal(section);
698+
});
699+
684700
it('should focus the previous widget on focused widget removal', async () => {
685701
const sectionWidget = getElementFromCell(dashboard, 1, 1)!;
686702
getParentSection(sectionWidget)!.focus();

0 commit comments

Comments
 (0)