The .textContent property returns (or set) the text content of all children and since the <button> element may contain further nested elements, its strict content might be mixed with promiscuous content coming from other elements like your span icon.
Here in this demo I added a layer that will convert the button content so that every single text node found will be removed and changed to a span.textnode so that later when you'll need to style the text content adding the <mark> it will be strightforward to change ONLY the contents of inner span.textnode elements.
To better show the concept I also added the fontawesome asset and style the icon inside the button.
const buttons = document.querySelectorAll("button");
function addHighlight(buttons, text){
buttons.forEach(button => {
initButton(button);
highlightTextInButton(button, text);
});
}
function initButton(button){
//for each child node in button
for (var i = 0; i < button.childNodes.length; i++) {
const child = button.childNodes[i];
//if it's of type TEXT_NODE
if (child.nodeType === Node.TEXT_NODE) {
//creates a new span.textnode
const span = document.createElement('span');
span.classList.add('textnode');
//with this same content
span.textContent = child.nodeValue;
//removes the text node
child.remove();
//adds the new span.textnode
button.append(span);
}
}
}
function highlightTextInButton(button, text){
const regex = new RegExp(text);
button.querySelectorAll(':scope > .textnode')
.forEach(textnode=>{
const textContent = textnode.textContent;
textnode.innerHTML = textContent.replace(regex,`<mark>$&</mark>`);
});
}
addHighlight(buttons, "T");
.textnode{
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css" integrity="sha512-MV7K8+y+gLIBoVD59lQIYicR65iaqukzvf/nwasF0nqhPay5w/9lJmVM2hMDcnK1OnMGCdVK+iQrJ7lzPJQd1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<button>
<i class="fa fa-star"></i>
Text
</button>