Skip to content

Commit e70745d

Browse files
authored
feat: add ability to ignore elements in SlotController (#10463)
1 parent ee9ad13 commit e70745d

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
@@ -335,6 +335,166 @@ describe('SlotController', () => {
335335
});
336336
});
337337

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

0 commit comments

Comments
 (0)