Scroll the document, and let the chrome scroll with it #110
@@ -293,8 +293,8 @@ Two things only the document scroller gets: **keyboard paging** unconditionally
|
|||||||
takes that height with **`.scroll-region`** and carries the flex chain down to it, because only the
|
takes that height with **`.scroll-region`** and carries the flex chain down to it, because only the
|
||||||
page knows its own tree. The shell must never go looking through a page for a component it
|
page knows its own tree. The shell must never go looking through a page for a component it
|
||||||
recognises — a rule keyed on `.table-wrap` works for a table the content slot holds directly and
|
recognises — a rule keyed on `.table-wrap` works for a table the content slot holds directly and
|
||||||
silently clips one nested any deeper. `data-table` takes `fills: true` for exactly this, since its
|
silently clips one nested any deeper. `data-table` takes `scrollRegion: true` for exactly this, since
|
||||||
wrapper is host markup a page cannot put a class on. The `visual.spec.ts` scroll test presses **End** rather than sending a
|
its wrapper is host markup a page cannot put the class on itself. The `visual.spec.ts` scroll test presses **End** rather than sending a
|
||||||
wheel event (Firefox's synthetic wheel does not reach the document) and rather than
|
wheel event (Firefox's synthetic wheel does not reach the document) and rather than
|
||||||
`scrollIntoView`, which a script can apply to an overflow-hidden box that no reader can scroll.
|
`scrollIntoView`, which a script can apply to an overflow-hidden box that no reader can scroll.
|
||||||
- **`ICON_NAMES` (`src/ui/icons.ts`) is a host-owned registry, not a frozen plugin contract**, so it
|
- **`ICON_NAMES` (`src/ui/icons.ts`) is a host-owned registry, not a frozen plugin contract**, so it
|
||||||
|
|||||||
+4
-4
@@ -16,8 +16,8 @@ own wrapper was a flex child with `overflow-y: auto`. `.table-wrap` and `.shell-
|
|||||||
else was, and a long page in `.form-page` clipped everything past the window in every engine.
|
else was, and a long page in `.form-page` clipped everything past the window in every engine.
|
||||||
|
|
||||||
Now the shell is `min-height: 100dvh` and the document scrolls. The sidebar is `position: sticky` at
|
Now the shell is `min-height: 100dvh` and the document scrolls. The sidebar is `position: sticky` at
|
||||||
full height and the topbar sticks with it, so both stay put as the page flows. Keyboard paging, back/
|
full height and the topbar sticks with it, so both stay put as the page flows. Keyboard paging and
|
||||||
forward scroll restoration and find-in-page work without a page doing anything.
|
back/forward scroll restoration work without a page doing anything.
|
||||||
|
|
||||||
### A bounded frame is `fill: true`, and the page says what fills it
|
### A bounded frame is `fill: true`, and the page says what fills it
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ The two halves are split on purpose. The shell bounds the content column, becaus
|
|||||||
shell's to give and a page computing it would have to know the topbar's own. The page marks the
|
shell's to give and a page computing it would have to know the topbar's own. The page marks the
|
||||||
element that takes that height with `.scroll-region`, because only the page knows its own tree — a
|
element that takes that height with `.scroll-region`, because only the page knows its own tree — a
|
||||||
host rule keyed on a component would work for a table held directly by the content slot and silently
|
host rule keyed on a component would work for a table held directly by the content slot and silently
|
||||||
clip one nested any deeper. `data-table` takes `fills: true`, which puts the class on its own wrapper.
|
clip one nested any deeper. `data-table` takes `scrollRegion: true`, which puts the class on its own wrapper.
|
||||||
|
|
||||||
### Upgrading a plugin
|
### Upgrading a plugin
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ clip one nested any deeper. `data-table` takes `fills: true`, which puts the cla
|
|||||||
`.scroll-region` on that region, and makes every wrapper between it and the content slot a flex
|
`.scroll-region` on that region, and makes every wrapper between it and the content slot a flex
|
||||||
column (`display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0`). Miss a wrapper and
|
column (`display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0`). Miss a wrapper and
|
||||||
the region grows instead of scrolling, and `fill`'s bounded box clips it.
|
the region grows instead of scrolling, and `fill`'s bounded box clips it.
|
||||||
4. A table whose header must stay put passes `fills: true` to `data-table` on such a page.
|
4. A table whose header must stay put passes `scrollRegion: true` to `data-table` on such a page.
|
||||||
`examples/plugins/scheduling` shows the whole chain on its shifts list.
|
`examples/plugins/scheduling` shows the whole chain on its shifts list.
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|||||||
@@ -940,7 +940,7 @@ A page that is a bounded frame instead — a board of full-height columns, a tab
|
|||||||
stay put — passes **`fill: true`** to the shell, which bounds the content column. The page then says
|
stay put — passes **`fill: true`** to the shell, which bounds the content column. The page then says
|
||||||
what fills it: put **`.scroll-region`** on that element, and make every wrapper between it and the
|
what fills it: put **`.scroll-region`** on that element, and make every wrapper between it and the
|
||||||
content slot a flex column (`display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0`).
|
content slot a flex column (`display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0`).
|
||||||
A `data-table` takes `fills: true`, which puts the class on its own wrapper — its sticky header needs
|
A `data-table` takes `scrollRegion: true`, which puts the class on its own wrapper — its sticky header needs
|
||||||
it, since a header only sticks to a scrollport that moves. `examples/plugins/scheduling` shows the
|
it, since a header only sticks to a scrollport that moves. `examples/plugins/scheduling` shows the
|
||||||
whole chain on its shifts list.
|
whole chain on its shifts list.
|
||||||
|
|
||||||
|
|||||||
@@ -210,16 +210,17 @@ test.describe.serial("authenticated admin journey", () => {
|
|||||||
|
|
||||||
const bounded = await page.evaluate(() => {
|
const bounded = await page.evaluate(() => {
|
||||||
const wrap = document.querySelector(".table-wrap");
|
const wrap = document.querySelector(".table-wrap");
|
||||||
if (!(wrap instanceof HTMLElement)) return null;
|
const frame = document.querySelector(".app-fill");
|
||||||
|
if (!(wrap instanceof HTMLElement) || !(frame instanceof HTMLElement)) return null;
|
||||||
return {
|
return {
|
||||||
documentScrolls: document.documentElement.scrollHeight > window.innerHeight,
|
frameScrolls: frame.scrollHeight > frame.clientHeight,
|
||||||
regionScrolls: wrap.scrollHeight > wrap.clientHeight,
|
regionScrolls: wrap.scrollHeight > wrap.clientHeight,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
expect(bounded, ".table-wrap must render").not.toBeNull();
|
expect(bounded, ".table-wrap and .app-fill must render").not.toBeNull();
|
||||||
// Both halves: the shell bounds the column, and the page's chain hands that height to the table.
|
// Both halves, and both discriminating: miss a wrapper in the page's chain and the table grows
|
||||||
// Miss a wrapper and the region grows instead, which is what the bounded box would then clip.
|
// instead of scrolling, which pushes the frame past its own height.
|
||||||
expect(bounded?.documentScrolls, "the document must not scroll on a filled page").toBe(false);
|
expect(bounded?.frameScrolls, "a filled page must fit its frame").toBe(false);
|
||||||
expect(bounded?.regionScrolls, "the table must be the thing that scrolls").toBe(true);
|
expect(bounded?.regionScrolls, "the table must be the thing that scrolls").toBe(true);
|
||||||
|
|
||||||
const headTop = async () => (await page.locator("thead th").first().boundingBox())?.y ?? -1;
|
const headTop = async () => (await page.locator("thead th").first().boundingBox())?.y ?? -1;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
%><%
|
%><%
|
||||||
const navHtml = include("partials/nav-tree", { nodes: chrome.nav });
|
const navHtml = include("partials/nav-tree", { nodes: chrome.nav });
|
||||||
const filtersHtml = include("partials/filter-bar", filterBar);
|
const filtersHtml = include("partials/filter-bar", filterBar);
|
||||||
const tableHtml = include("partials/data-table", { ...table, fills: true });
|
const tableHtml = include("partials/data-table", { ...table, scrollRegion: true });
|
||||||
const alertHtml = locals.error ? include("partials/alert", { text: locals.error, tone: "neg" }) : "";
|
const alertHtml = locals.error ? include("partials/alert", { text: locals.error, tone: "neg" }) : "";
|
||||||
const actions = canWrite
|
const actions = canWrite
|
||||||
? '<a class="btn btn-primary" href="' + localeHref(newHref) + '"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-plus"/></svg>' + t("scheduling.shifts.new") + '</a>'
|
? '<a class="btn btn-primary" href="' + localeHref(newHref) + '"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-plus"/></svg>' + t("scheduling.shifts.new") + '</a>'
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ th[aria-sort="descending"] .sort-ico { transform: rotate(180deg); }
|
|||||||
document. For a page whose whole point is a bounded frame — a board of full-height columns, a table
|
document. For a page whose whole point is a bounded frame — a board of full-height columns, a table
|
||||||
whose header must stay put. The height is the shell's to give: a page computing it would have to
|
whose header must stay put. The height is the shell's to give: a page computing it would have to
|
||||||
know the topbar's own. What fills that height is the page's to say — `.scroll-region` below. */
|
know the topbar's own. What fills that height is the page's to say — `.scroll-region` below. */
|
||||||
.app-fill { height: 100dvh; overflow: hidden; }
|
.app-fill { height: 100dvh; overflow: auto; }
|
||||||
.app-fill .content { min-height: 0; }
|
.app-fill .content { min-height: 0; }
|
||||||
|
|
||||||
/* Marks the one element that takes the leftover height and scrolls. Every ancestor between it and
|
/* Marks the one element that takes the leftover height and scrolls. Every ancestor between it and
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
caption?, selectable?, actions? sr-only caption; toggle the check / kebab columns
|
caption?, selectable?, actions? sr-only caption; toggle the check / kebab columns
|
||||||
actionsId? id stem for the row-action menus (default `row-actions`);
|
actionsId? id stem for the row-action menus (default `row-actions`);
|
||||||
name it when two tables share a page
|
name it when two tables share a page
|
||||||
fills? take the leftover height and scroll, so the header stays
|
scrollRegion? take the leftover height and scroll, so the header stays
|
||||||
put — needs the shell's `fill: true` and a flex chain
|
put — needs the shell's `fill: true` and a flex chain
|
||||||
above (README → the app shell)
|
above (README → the app shell)
|
||||||
columns: { label, sortable?, sort?: "asc"|"desc", href?, className? }[]
|
columns: { label, sortable?, sort?: "asc"|"desc", href?, className? }[]
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
const rows = locals.rows || [];
|
const rows = locals.rows || [];
|
||||||
const emptyText = locals.emptyText || t("table.empty"); // shown when a table that has columns has no rows
|
const emptyText = locals.emptyText || t("table.empty"); // shown when a table that has columns has no rows
|
||||||
const actionsId = locals.actionsId || "row-actions";
|
const actionsId = locals.actionsId || "row-actions";
|
||||||
const fills = locals.fills === true;
|
const scrollRegion = locals.scrollRegion === true;
|
||||||
-%>
|
-%>
|
||||||
<div class="table-wrap<%= fills ? " scroll-region" : "" %>">
|
<div class="table-wrap<%= scrollRegion ? " scroll-region" : "" %>">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<% if (caption) { -%>
|
<% if (caption) { -%>
|
||||||
<caption class="sr-only"><%= caption %></caption>
|
<caption class="sr-only"><%= caption %></caption>
|
||||||
|
|||||||
Reference in New Issue
Block a user