Skip to content

Commit b0447fd

Browse files
feat: add ability to ignore elements in SlotController (#10463) (#10466)
Co-authored-by: Tomi Virkki <tomivirkki@users.noreply.github.com>
1 parent 5596040 commit b0447fd

File tree

2 files changed

+171
-1
lines changed

2 files changed

+171
-1
lines changed

packages/component-base/src/slot-controller.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export class SlotController extends EventTarget {
129129
getSlotChildren() {
130130
const { slotName } = this;
131131
return Array.from(this.host.childNodes).filter((node) => {
132+
// Ignore nodes with data-slot-ignore attribute
133+
if (node.nodeType === Node.ELEMENT_NODE && node.hasAttribute('data-slot-ignore')) {
134+
return false;
135+
}
132136
// Either an element (any slot) or a text node (only un-named slot).
133137
return (
134138
(node.nodeType === Node.ELEMENT_NODE && node.slot === slotName) ||
@@ -203,7 +207,13 @@ export class SlotController extends EventTarget {
203207

204208
// Calling `slot.assignedNodes()` includes whitespace text nodes in case of default slot:
205209
// unlike comment nodes, they are not filtered out. So we need to manually ignore them.
206-
const newNodes = addedNodes.filter((node) => !isEmptyTextNode(node) && !current.includes(node));
210+
// Also ignore nodes with data-slot-ignore attribute.
211+
const newNodes = addedNodes.filter(
212+
(node) =>
213+
!isEmptyTextNode(node) &&
214+
!current.includes(node) &&
215+
!(node.nodeType === Node.ELEMENT_NODE && node.hasAttribute('data-slot-ignore')),
216+
);
207217

208218
if (removedNodes.length) {
209219
this.nodes = current.filter((node) => !removedNodes.includes(node));

packages/component-base/test/slot-controller.test.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,166 @@ const runTests = (defineHelper, baseMixin) => {
336336
});
337337
});
338338

339+
describe('data-slot-ignore attribute', () => {
340+
let defaultNode;
341+
342+
describe('single slot with observe enabled', () => {
343+
beforeEach(async () => {
344+
element = fixtureSync(`<${tag}></${tag}>`);
345+
controller = new SlotController(element, 'foo', 'div', {
346+
initializer: (node) => {
347+
node.textContent = 'default content';
348+
},
349+
});
350+
element.addController(controller);
351+
defaultNode = element.querySelector('[slot="foo"]');
352+
// Wait for initial slotchange event
353+
await nextFrame();
354+
});
355+
356+
it('should ignore element with data-slot-ignore when checking slot children', () => {
357+
const ignored = document.createElement('div');
358+
ignored.setAttribute('slot', 'foo');
359+
ignored.setAttribute('data-slot-ignore', '');
360+
ignored.textContent = 'ignored';
361+
element.appendChild(ignored);
362+
363+
const slotChildren = controller.getSlotChildren();
364+
expect(slotChildren).to.not.include(ignored);
365+
expect(slotChildren).to.include(defaultNode);
366+
});
367+
368+
it('should not remove default node when element with data-slot-ignore is added', async () => {
369+
const ignored = document.createElement('div');
370+
ignored.setAttribute('slot', 'foo');
371+
ignored.setAttribute('data-slot-ignore', '');
372+
ignored.textContent = 'ignored';
373+
element.appendChild(ignored);
374+
375+
await nextFrame();
376+
expect(defaultNode.isConnected).to.be.true;
377+
expect(defaultNode.textContent).to.equal('default content');
378+
});
379+
380+
it('should remove default node when non-ignored element is added', async () => {
381+
const ignored = document.createElement('div');
382+
ignored.setAttribute('slot', 'foo');
383+
ignored.setAttribute('data-slot-ignore', '');
384+
ignored.textContent = 'ignored';
385+
element.appendChild(ignored);
386+
387+
await nextFrame();
388+
expect(defaultNode.isConnected).to.be.true;
389+
390+
const custom = document.createElement('div');
391+
custom.setAttribute('slot', 'foo');
392+
custom.textContent = 'custom';
393+
element.appendChild(custom);
394+
395+
await nextFrame();
396+
expect(defaultNode.isConnected).to.be.false;
397+
expect(controller.node).to.equal(custom);
398+
});
399+
400+
it('should not call initCustomNode for element with data-slot-ignore', async () => {
401+
const initSpy = sinon.spy(controller, 'initCustomNode');
402+
403+
const ignored = document.createElement('div');
404+
ignored.setAttribute('slot', 'foo');
405+
ignored.setAttribute('data-slot-ignore', '');
406+
ignored.textContent = 'ignored';
407+
element.appendChild(ignored);
408+
409+
await nextFrame();
410+
expect(initSpy.called).to.be.false;
411+
});
412+
});
413+
414+
describe('multiple slot with observe enabled', () => {
415+
let defaultNode;
416+
417+
beforeEach(async () => {
418+
element = fixtureSync(`<${tag}></${tag}>`);
419+
controller = new SlotController(element, '', 'div', {
420+
initializer: (node) => {
421+
node.textContent = 'default content';
422+
},
423+
multiple: true,
424+
});
425+
element.addController(controller);
426+
defaultNode = element.querySelector(':not([slot])');
427+
// Wait for initial slotchange event
428+
await nextFrame();
429+
});
430+
431+
it('should ignore element with data-slot-ignore in multiple mode', () => {
432+
const ignored = document.createElement('div');
433+
ignored.setAttribute('data-slot-ignore', '');
434+
ignored.textContent = 'ignored';
435+
element.appendChild(ignored);
436+
437+
const slotChildren = controller.getSlotChildren();
438+
expect(slotChildren).to.not.include(ignored);
439+
expect(slotChildren).to.include(defaultNode);
440+
});
441+
442+
it('should not remove default node when element with data-slot-ignore is added in multiple mode', async () => {
443+
const ignored = document.createElement('div');
444+
ignored.setAttribute('data-slot-ignore', '');
445+
ignored.textContent = 'ignored';
446+
element.appendChild(ignored);
447+
448+
await nextFrame();
449+
expect(defaultNode.isConnected).to.be.true;
450+
expect(controller.nodes).to.include(defaultNode);
451+
expect(controller.nodes).to.not.include(ignored);
452+
});
453+
454+
it('should remove default node when non-ignored element is added in multiple mode', async () => {
455+
const ignored = document.createElement('div');
456+
ignored.setAttribute('data-slot-ignore', '');
457+
ignored.textContent = 'ignored';
458+
element.appendChild(ignored);
459+
460+
await nextFrame();
461+
expect(defaultNode.isConnected).to.be.true;
462+
463+
const custom = document.createElement('div');
464+
custom.textContent = 'custom';
465+
element.appendChild(custom);
466+
467+
await nextFrame();
468+
expect(defaultNode.isConnected).to.be.false;
469+
expect(controller.nodes).to.include(custom);
470+
expect(controller.nodes).to.not.include(defaultNode);
471+
expect(controller.nodes).to.not.include(ignored);
472+
});
473+
474+
it('should allow multiple custom elements alongside ignored elements', async () => {
475+
const custom1 = document.createElement('div');
476+
custom1.textContent = 'custom1';
477+
element.appendChild(custom1);
478+
479+
const ignored = document.createElement('div');
480+
ignored.setAttribute('data-slot-ignore', '');
481+
ignored.textContent = 'ignored';
482+
element.appendChild(ignored);
483+
484+
const custom2 = document.createElement('div');
485+
custom2.textContent = 'custom2';
486+
element.appendChild(custom2);
487+
488+
await nextFrame();
489+
490+
expect(defaultNode.isConnected).to.be.false;
491+
expect(controller.nodes).to.have.lengthOf(2);
492+
expect(controller.nodes).to.include(custom1);
493+
expect(controller.nodes).to.include(custom2);
494+
expect(controller.nodes).to.not.include(ignored);
495+
});
496+
});
497+
});
498+
339499
describe('multiple nodes', () => {
340500
let children, initializeSpy;
341501

0 commit comments

Comments
 (0)