ReviewOS

stacks/stx

Component scope wrapper <div> is hoisted out of <table>, detaching client behaviour from <th>/<td>

#1980
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Summary

Components that render a table-section element — <th>, <td>, <tr>, <thead>, <tbody> — are wrapped at render time in <div data-stx-scope="…">. A <div> is not permitted there, so the HTML parser hoists the wrapper out of the table. The element itself survives in the right place, but its scope wrapper, and therefore its client behaviour, ends up somewhere else on the page.

Reproduction

<table class="min-w-full">
  <TableHead><TableRow><TableHeader>Date</TableHeader><TableHeader>Session</TableHeader></TableRow></TableHead>
  <TableBody><TableRow><TableCell>Mon</TableCell><TableCell>Intervals</TableCell></TableRow></TableBody>
</table>

Server output — note the <div> between <tr> and <th>:

<tr class="hover:bg-surface">
  <div data-stx-scope="stx_table_header_16_…">
    <th class="px-6 py-3 …">Date</th>
  </div>
  <script data-stx-scoped data-stx-owner="stx_table_header_16_…" …></script>
  …

After parsing:

[...document.querySelectorAll('[data-stx-scope^="stx_table"]')].map(s => s.parentElement.tagName)
// ["SECTION", "SECTION"]          ← not TR

[...document.querySelectorAll('[data-stx-scope^="stx_table"]')].map(s => !!s.closest('table'))
// [false, false]                  ← outside the table entirely

[...table.querySelectorAll('th')].map(x => x.parentElement.tagName)
// ["TR", "TR"]                    ← the <th> itself is fine

The wrappers are relocated to before the <table>, as empty divs in the parent <section>.

Impact

TableHeader takes a sortable prop and emits @click. That handler is registered against the scope wrapper — which is now an empty div outside the table — while the <th> the user actually clicks has no scope. Sorting a table by clicking its header cannot work.

More generally: any component in this package intended to render inside a <table> cannot carry client behaviour.

Note

This is independent of #1975. That one stops <Table> rendering its children at all; this one applies to the sub-components when they are used inside a plain <table>, which is currently the only way they render.

Expected

For a component whose root element is a table-section element, the renderer should not emit a wrapper element — e.g. put data-stx-scope on the component's own root, or use a comment/marker-based scope rather than a <div>. The same applies to any other content model that rejects <div>: <select> (<option>), <ul>/<ol> (<li>), <dl> (<dt>/<dd>).

Environment

  • @stacksjs/components 0.2.339, stacks 0.75.31, Bun 1.3.x, macOS
  • Found while migrating hq.training onto the component library.

Sign in to comment on this issue.