Inspect the fixed example
The fixed click targets a span inside the preview button. The listener belongs to tools. Resolve the nearest action button before reading data-action.
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="tools"><button type="button" data-action="preview"><span id="label">Preview source</span></button><p id="plain">Plain note</p></section><output id="result"></output>
<script>const tools=document.querySelector('#tools');
tools.addEventListener('click',event=>{const button=event.target.closest('button[data-action]');if(button&&tools.contains(button))document.querySelector('#result').textContent=button.dataset.action;});
document.querySelector('#label').click();console.log(document.querySelector('#result').textContent);
</script>preview| Inspection | Supplied input or action | Result |
|---|---|---|
| target | #label span | Has no data-action |
| currentTarget | #tools section | Owns listener |
| closest | button[data-action] | preview |
Follow the native mechanism
event.target identifies the original target; currentTarget identifies the listener’s container during the callback. closest includes the target itself and searches its ancestors. The contains check keeps the selected button inside tools. This fixed event target is an Element; a general handler must check that assumption.
Compare the material alternative
Reading target.dataset.action directly sees undefined on the span, although the enclosing button holds preview.
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="tools"><button type="button" data-action="preview"><span id="label">Preview source</span></button></section><output id="result"></output>
<script>let observed;document.querySelector('#tools').addEventListener('click',event=>{observed=event.target.dataset.action;});document.querySelector('#label').click();
const result={actionType:typeof observed};
document.querySelector('#result').textContent=JSON.stringify(result);
console.log(JSON.stringify(result));
</script>{"actionType":"undefined"}Work the boundary
Will clicking a plain paragraph write an action, and does clicking the button itself still work?
Show the worked answer
The paragraph is ignored. The button matches closest itself and resolves preview; the trace identifies both targets and the shared container.
<style>html{font-size:16px}body{margin:12px;font:1rem/1.5 system-ui,sans-serif;color:#17332d;background:#fff}p{overflow-wrap:anywhere}output{display:block;margin-block:12px;overflow-wrap:anywhere;white-space:pre-wrap}button,input{font:inherit;max-width:100%;box-sizing:border-box}button{margin:4px;padding:8px}label{display:block;margin-block:8px}button:focus-visible,input:focus-visible{outline:3px solid #23715d;outline-offset:2px}ul{padding-inline-start:24px}li{overflow-wrap:anywhere}.highlight{border:2px solid green}</style>
<section id="tools"><button id="action" type="button" data-action="preview"><span id="label">Preview source</span></button><p id="plain">Plain note</p></section><output id="result"></output>
<script>const tools=document.querySelector('#tools');const trace=[];tools.addEventListener('click',event=>{const button=event.target.closest('button[data-action]');trace.push({target:event.target.id,currentTarget:event.currentTarget.id,action:button&&tools.contains(button)?button.dataset.action:null});});
document.querySelector('#plain').click();document.querySelector('#action').click();const result=trace;
document.querySelector('#result').textContent=JSON.stringify(result);
console.log(JSON.stringify(result));
</script>[{"target":"plain","currentTarget":"tools","action":null},{"target":"action","currentTarget":"tools","action":"preview"}]