#301: the token tables need the same scroll wrapper
The sorted report finally named it: `table.api-tokens__table` at 833px wide, with its `.visually-hidden` heading reaching right=737 — exactly the document's scrollWidth. Same mechanism as the sessions table, a second table I had not wrapped. Locally the API-tokens table was empty and therefore narrow, which is why this only ever appeared in CI. With a token present it reproduces: without the wrapper 345px of page overflow, with it none. The feed-token table gets the same treatment — it is built the same way and would fail as soon as someone holds a feed token with a long name. The "[in fitting scroller]" marker in the report is misleading for these: `main.main` is a scroller, but it is `position: static`, so it never clipped the absolutely positioned heading. Only a positioned ancestor does — which is what `.table-scroll` now is. Verified locally against a real stack, with a wide token table present: reflow guard green, whole a11y pack green in both colour schemes.
This commit is contained in:
parent
2422f3a28f
commit
69882ecbea
@ -172,52 +172,54 @@ function TokenList({ tokens }: { tokens: ApiTokenView[] }): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormError error={error} />
|
<FormError error={error} />
|
||||||
<table className="table api-tokens__table">
|
<div className="table-scroll" tabIndex={0} role="region" aria-label={t('section.title')}>
|
||||||
<thead>
|
<table className="table api-tokens__table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>{t('fields.name')}</th>
|
<tr>
|
||||||
<th>{t('fields.scope')}</th>
|
<th>{t('fields.name')}</th>
|
||||||
<th>{t('fields.ponds')}</th>
|
<th>{t('fields.scope')}</th>
|
||||||
<th>{t('list.created')}</th>
|
<th>{t('fields.ponds')}</th>
|
||||||
<th>{t('list.lastUsed')}</th>
|
<th>{t('list.created')}</th>
|
||||||
<th>{t('list.expires')}</th>
|
<th>{t('list.lastUsed')}</th>
|
||||||
<th>{t('list.status')}</th>
|
<th>{t('list.expires')}</th>
|
||||||
<th>
|
<th>{t('list.status')}</th>
|
||||||
<span className="visually-hidden">{t('common:tableActions')}</span>
|
<th>
|
||||||
</th>
|
<span className="visually-hidden">{t('common:tableActions')}</span>
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{tokens.map((token) => (
|
|
||||||
<tr key={token.id}>
|
|
||||||
<td>{token.name}</td>
|
|
||||||
<td>{token.scope === 'write' ? t('fields.scopeWrite') : t('fields.scopeRead')}</td>
|
|
||||||
<td>
|
|
||||||
{token.ponds.length === 0
|
|
||||||
? t('list.allPonds')
|
|
||||||
: token.ponds.map((pond) => pond.name).join(', ')}
|
|
||||||
</td>
|
|
||||||
<td>{new Date(token.createdAt).toLocaleDateString()}</td>
|
|
||||||
<td>
|
|
||||||
{token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : t('list.never')}
|
|
||||||
</td>
|
|
||||||
<td>{token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : '—'}</td>
|
|
||||||
<td>{t(`list.${statusOf(token)}`)}</td>
|
|
||||||
<td>
|
|
||||||
{!token.revokedAt && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button api-tokens__revoke"
|
|
||||||
onClick={() => void revoke(token.id)}
|
|
||||||
>
|
|
||||||
{t('list.revoke')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{tokens.map((token) => (
|
||||||
|
<tr key={token.id}>
|
||||||
|
<td>{token.name}</td>
|
||||||
|
<td>{token.scope === 'write' ? t('fields.scopeWrite') : t('fields.scopeRead')}</td>
|
||||||
|
<td>
|
||||||
|
{token.ponds.length === 0
|
||||||
|
? t('list.allPonds')
|
||||||
|
: token.ponds.map((pond) => pond.name).join(', ')}
|
||||||
|
</td>
|
||||||
|
<td>{new Date(token.createdAt).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
{token.lastUsedAt ? new Date(token.lastUsedAt).toLocaleString() : t('list.never')}
|
||||||
|
</td>
|
||||||
|
<td>{token.expiresAt ? new Date(token.expiresAt).toLocaleDateString() : '—'}</td>
|
||||||
|
<td>{t(`list.${statusOf(token)}`)}</td>
|
||||||
|
<td>
|
||||||
|
{!token.revokedAt && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button api-tokens__revoke"
|
||||||
|
onClick={() => void revoke(token.id)}
|
||||||
|
>
|
||||||
|
{t('list.revoke')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,32 +85,38 @@ export function FeedTokensSection(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
{tokens.data && tokens.data.length === 0 && <p>{t('feed.empty')}</p>}
|
{tokens.data && tokens.data.length === 0 && <p>{t('feed.empty')}</p>}
|
||||||
{tokens.data && tokens.data.length > 0 && (
|
{tokens.data && tokens.data.length > 0 && (
|
||||||
<table className="table">
|
<div className="table-scroll" tabIndex={0} role="region" aria-label={t('feed.title')}>
|
||||||
<thead>
|
<table className="table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>{t('fields.name')}</th>
|
<tr>
|
||||||
<th>{t('list.created')}</th>
|
<th>{t('fields.name')}</th>
|
||||||
<th>{t('list.lastUsed')}</th>
|
<th>{t('list.created')}</th>
|
||||||
<th>
|
<th>{t('list.lastUsed')}</th>
|
||||||
<span className="visually-hidden">{t('common:tableActions')}</span>
|
<th>
|
||||||
</th>
|
<span className="visually-hidden">{t('common:tableActions')}</span>
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{tokens.data.map((token) => (
|
|
||||||
<tr key={token.id}>
|
|
||||||
<td>{token.name}</td>
|
|
||||||
<td>{formatTime(token.createdAt)}</td>
|
|
||||||
<td>{token.lastUsedAt ? formatTime(token.lastUsedAt) : '—'}</td>
|
|
||||||
<td>
|
|
||||||
<button type="button" className="linklike" onClick={() => void remove(token.id)}>
|
|
||||||
{t('feed.delete')}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{tokens.data.map((token) => (
|
||||||
|
<tr key={token.id}>
|
||||||
|
<td>{token.name}</td>
|
||||||
|
<td>{formatTime(token.createdAt)}</td>
|
||||||
|
<td>{token.lastUsedAt ? formatTime(token.lastUsedAt) : '—'}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="linklike"
|
||||||
|
onClick={() => void remove(token.id)}
|
||||||
|
>
|
||||||
|
{t('feed.delete')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user