Make a table's density the caller's decision
Density was applied to every fine-pointer device with no way out, which M3 forbids outright: "don't apply density by default; offer an explicit density opt-in instead, keeping opt-out targets at >= 48x48 CSS px". A row is now 52px and `dense` is a prop that tightens it to 36. The row divider also takes outline-variant itself rather than 60% of it, since M3 keeps opacity for state layers and disabled. Plan step 20, findings IN-16 and IN-30. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5989e9ea92
commit
42d62ed00c
@@ -831,7 +831,7 @@ A choice of colour profile (see Colour profiles): a swatch per generated profile
|
|||||||
|
|
||||||
### `<x-table>`, `<x-sort-header>`
|
### `<x-table>`, `<x-sort-header>`
|
||||||
|
|
||||||
A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>` (`size="xs"` for a dense one); cell utilities (`text-end`, `whitespace-nowrap`) always win. Scrolling is yours: wrap it in `<div class="overflow-x-auto">`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`.
|
A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>`; cell utilities (`text-end`, `whitespace-nowrap`) always win. Rows are 52px — a target a finger can hit. `dense` tightens them to 36px and `size="xs"` is for a table inside a panel inside a panel (32px rows); M3 says density is always an opt-in, so neither is a default and both are yours to justify. Scrolling is yours: wrap it in `<div class="overflow-x-auto">`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`.
|
||||||
|
|
||||||
`<x-sort-header column="size" :sort-by="$sortBy">Size</x-sort-header>` sorts through the Livewire property `sortBy` (`['column' => …, 'direction' => 'asc'|'desc']`; `model` names another), with `aria-sort`.
|
`<x-sort-header column="size" :sort-by="$sortBy">Size</x-sort-header>` sorts through the Livewire property `sortBy` (`['column' => …, 'direction' => 'asc'|'desc']`; `model` names another), with `aria-sort`.
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,14 @@
|
|||||||
* So the styling is descendant selectors on the one attribute the component sets, all inside
|
* So the styling is descendant selectors on the one attribute the component sets, all inside
|
||||||
* `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always
|
* `:where()` and `@layer components`: a caller's `text-end` or `whitespace-nowrap` on a cell always
|
||||||
* wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in
|
* wins. Header cells in title-small on-surface-variant over an outline-variant rule, body cells in
|
||||||
* body-medium between faint rules. One density step tighter on a fine pointer — keyed on the
|
* body-medium between outline-variant rules. A row that opens something is `data-list-row` and
|
||||||
* pointer, not the width, so a touch tablet in landscape keeps rows a finger can hit. A row that
|
* answers a pointer as a list row does (components/list.css).
|
||||||
* opens something is `data-list-row` and answers a pointer as a list row does (components/list.css).
|
*
|
||||||
|
* A row is 52px: 16px above and below a body-medium line. Density is never applied by itself —
|
||||||
|
* "don't apply density by default; offer an explicit density opt-in instead, keeping opt-out targets
|
||||||
|
* at >= 48x48 CSS px" (docs/reference/m3/foundations-supplement.md § Accessibility) — so `dense`
|
||||||
|
* (`data-dense`) is the caller's decision, and it is the caller's to justify: a dense row is 36px,
|
||||||
|
* and a dense row inside an `xs` table 24px.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -16,7 +21,7 @@
|
|||||||
viewport. */
|
viewport. */
|
||||||
[data-table] {
|
[data-table] {
|
||||||
--cell-x: 0.75rem;
|
--cell-x: 0.75rem;
|
||||||
--cell-y: 0.75rem;
|
--cell-y: 1rem;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -34,14 +39,12 @@
|
|||||||
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (pointer: fine) {
|
[data-table][data-dense] {
|
||||||
[data-table] {
|
--cell-y: 0.5rem;
|
||||||
--cell-y: 0.5rem;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
[data-table][data-size="xs"] {
|
[data-table][data-size="xs"][data-dense] {
|
||||||
--cell-y: 0.25rem;
|
--cell-y: 0.25rem;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-table] :where(th, td) {
|
[data-table] :where(th, td) {
|
||||||
@@ -63,8 +66,9 @@
|
|||||||
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
letter-spacing: var(--md-sys-typescale-label-md-tracking);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The role itself, not a fraction of it: M3 reserves opacity for state layers and disabled. */
|
||||||
[data-table] :where(tbody tr) {
|
[data-table] :where(tbody tr) {
|
||||||
border-bottom: 1px solid color-mix(in srgb, var(--md-sys-color-outline-variant) 60%, transparent);
|
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-table] :where(tbody tr:last-child) {
|
[data-table] :where(tbody tr:last-child) {
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
look is resources/css/components/table.css, keyed on the `data-table` attribute set here (the
|
look is resources/css/components/table.css, keyed on the `data-table` attribute set here (the
|
||||||
top of that file says why).
|
top of that file says why).
|
||||||
|
|
||||||
`size="xs"` for a table inside a panel inside a panel. A selected row is
|
Rows are 52px, a target a finger can hit. `dense` tightens them to 36px — M3 asks that density
|
||||||
`<tr aria-selected="true">`. Horizontal scrolling stays the caller's — wrap the table in
|
always be an opt-in and never a default, so it is the caller's decision and the caller's to
|
||||||
`<div class="overflow-x-auto">` where the page needs it. A column that sorts is
|
justify. `size="xs"` for a table inside a panel inside a panel (32px rows, 24px with `dense`).
|
||||||
|
|
||||||
|
A selected row is `<tr aria-selected="true">`. Horizontal scrolling stays the caller's — wrap the
|
||||||
|
table in `<div class="overflow-x-auto">` where the page needs it. A column that sorts is
|
||||||
`<x-sort-header>`. --}}
|
`<x-sort-header>`. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'size' => 'sm',
|
'size' => 'sm',
|
||||||
|
'dense' => false,
|
||||||
])
|
])
|
||||||
|
|
||||||
<table data-table data-size="{{ $size === 'xs' ? 'xs' : 'sm' }}" {{ $attributes }}>
|
<table data-table data-size="{{ $size === 'xs' ? 'xs' : 'sm' }}"{{ $dense ? ' data-dense' : '' }} {{ $attributes }}>
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ it('marks a table for its styles and takes a size', function () {
|
|||||||
->and((string) $this->blade('<x-table size="huge" />'))->toContain('data-size="sm"');
|
->and((string) $this->blade('<x-table size="huge" />'))->toContain('data-size="sm"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tightens a table only when the caller asks for it', function () {
|
||||||
|
expect((string) $this->blade('<x-table />'))->not->toContain('data-dense')
|
||||||
|
->and((string) $this->blade('<x-table dense />'))->toContain('data-dense');
|
||||||
|
});
|
||||||
|
|
||||||
it('sorts by its column, ascending first and then flipping', function () {
|
it('sorts by its column, ascending first and then flipping', function () {
|
||||||
expect((string) $this->blade('<x-sort-header column="size" :sort-by="[\'column\' => \'name\', \'direction\' => \'asc\']">Size</x-sort-header>'))
|
expect((string) $this->blade('<x-sort-header column="size" :sort-by="[\'column\' => \'name\', \'direction\' => \'asc\']">Size</x-sort-header>'))
|
||||||
->not->toContain('aria-sort')
|
->not->toContain('aria-sort')
|
||||||
|
|||||||
Reference in New Issue
Block a user