rastrillo / fiesta Public

Rest the column preview at its page's height and grow it while a menu is open

The column's footer menus open downward under the page, so a preview
box sized for them open left about 300px of nothing under the page at
rest. The box now has two heights: the open one the tables already
hold, and a rest one in previewRestHeights (Desktop 650, Mobile 1110),
written beside it as --ds-hr and --ds-hmr. view.js marks the widget
data-ds-rest while no menu in its frame is open, and view.css then
hands the box the rest heights under the names its Desktop and Mobile
rules already read. css/ is untouched.

view.js listens in the framed document. A click on a closed menu's
summary unmarks the widget at once, before the menu opens: placed in
the short frame, an anchored panel opens up over the page and stays
there once the frame grows. toggle then settles the mark from what is
really open, which covers the keyboard, a menu closed by its sibling
opening (the two share a name), and a click whose menu did not open.

With scripts off the widget is never marked and keeps the open height.
That is the usable fallback: nothing could grow the box when a menu
opens, and a menu cut off at the box's foot is one a reader cannot
use, where empty space under the page costs nothing.

The height drives measure and enforce the two apart. A marked widget
reads its rest box at rest and is unmarked for a moment to read its
open box; what opens is held to the open box, what is at rest to the
rest box, and a key whose mark disagrees with previewRestHeights fails
the drive, since its readings would be of the wrong box. The narrow
drive names a rest entry to raise separately, and the sweep prints the
rest heights it measured. The numbers are from a sweep of the column
over all twelve locales: rest needs at most 621px on Desktop (ga) and
1085px on a 320px phone (es, pt-PT); the open heights hold in every
locale (es and pt-PT need 1327 of 1330).

Tests, red by assertion before the change:
TestTheColumnPreviewRestsShorterThanItOpens (no rest height for the
column) and TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen (at rest the
frame was 920 and 1330, want 650 and 1110). Mutations, each caught by
name: the toggle listener removed (the box stays open after closing),
the view.css rule removed (the box never rests), the rule applied
without the script's mark (the scripts-off leg rests at 650), the rest
Desktop at 540 and the rest Mobile at 1000 (the wide and narrow drives
name the rest shortfall), the open Desktop at 700 (the wide drive names
the open shortfall against the open box), and view.js never marking
(the drive refuses the readings). The new browser test is weighed
at 7 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 4306038a3d835258556228896eb4c1b249ebda12 parent 4ac98b3
6 files changed, +267 −15
  • hack/test-weights.txt +1 −0
  • internal/gallery/heights.go +30 −2
  • internal/gallery/heights_browser_test.go +134 −12
  • internal/gallery/heights_test.go +45 −0
  • internal/gallery/view.css +8 −0
  • internal/gallery/view.js +49 −1

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.209s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 153737 bytes, 22431 bytes gzipped (21.9 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 153832 bytes, 22542 bytes gzipped (22.0 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 156651 bytes, 23146 bytes gzipped (22.6 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.024s
=== history ===
history: 21 commits; release tags: v1.0.0
=== suite ===
--- shard 0 of 48 ---
==> shard 0 of 48: make root
go build ./...
go vet ./...
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh root 'internal/gallery@3 cmd/fiesta@2 internal/www@2'
ok  	amadan.net/rastrillo/fiesta	3.007s
ok  	amadan.net/rastrillo/fiesta/check	8.168s
ok  	amadan.net/rastrillo/fiesta/example	0.392s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.277s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.363s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.018s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.008s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.589s
ok  	amadan.net/rastrillo/fiesta/internal/site	3.479s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.011s
suite: root took 15s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m10.220000s 0m1.780000s
--- shard 1 of 48 ---
==> shard 1 of 48: make test.internal.gallery~1of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~1of3'
gotest: slice 1 of 3 of ./internal/gallery/ runs 36 of 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.984s
suite: test.internal.gallery~1of3 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.690000s 0m1.230000s
--- shard 2 of 48 ---
==> shard 2 of 48: make test.internal.gallery~2of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~2of3'
gotest: slice 2 of 3 of ./internal/gallery/ runs 35 of 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	62.496s
suite: test.internal.gallery~2of3 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.070000s 0m1.310000s
--- shard 3 of 48 ---
==> shard 3 of 48: make test.internal.gallery~3of3
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.gallery~3of3'
gotest: slice 3 of 3 of ./internal/gallery/ runs 36 of 107 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	57.087s
suite: test.internal.gallery~3of3 took 62s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.680000s 0m1.080000s
--- shard 4 of 48 ---
==> shard 4 of 48: make test.cmd.fiesta~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~1of2'
gotest: slice 1 of 2 of ./cmd/fiesta/ runs 7 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	48.548s
suite: test.cmd.fiesta~1of2 took 53s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.350000s 0m2.120000s
--- shard 5 of 48 ---
==> shard 5 of 48: make test.cmd.fiesta~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'cmd.fiesta~2of2'
gotest: slice 2 of 2 of ./cmd/fiesta/ runs 5 of 12 test tests
ok  	amadan.net/rastrillo/fiesta/cmd/fiesta	52.440s
suite: test.cmd.fiesta~2of2 took 56s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m35.120000s 0m2.360000s
--- shard 6 of 48 ---
==> shard 6 of 48: make test.internal.www~1of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~1of2'
gotest: slice 1 of 2 of ./internal/www/ runs 11 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	61.447s
suite: test.internal.www~1of2 took 67s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.350000s 0m1.520000s
--- shard 7 of 48 ---
==> shard 7 of 48: make test.internal.www~2of2
FIESTA_TEST_REQUIRE_NODE=1 FIESTA_TEST_REQUIRE_GIT=1 FIESTA_TEST_REQUIRE_TAGS=1 ./hack/gotest.sh test 'internal.www~2of2'
gotest: slice 2 of 2 of ./internal/www/ runs 14 of 25 test tests
ok  	amadan.net/rastrillo/fiesta/internal/www	59.391s
suite: test.internal.www~2of2 took 65s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.630000s 0m1.480000s
--- shard 8 of 48 ---
==> shard 8 of 48: make staticcheck
go run honnef.co/go/tools/cmd/staticcheck@v0.8.2 -tags browser ./...
suite: staticcheck took 12s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.730000s 0m0.880000s
--- shard 9 of 48 ---
==> shard 9 of 48: make govulncheck
go run golang.org/x/vuln/cmd/govulncheck@v1.8.0 ./...
=== Symbol Results ===

No vulnerabilities found.

Your code is affected by 0 vulnerabilities.
This scan also found 2 vulnerabilities in packages you import and 5
vulnerabilities in modules you require, but your code doesn't appear to call
these vulnerabilities.
Use '-show verbose' for more details.
suite: govulncheck took 9s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m3.080000s 0m0.360000s
--- shard 10 of 48 ---
==> shard 10 of 48: make gitleaks
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --no-banner --redact --log-opts='--full-history --diff-filter=tuxdb HEAD' .
3:46AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
3:46AM INF 21 commits scanned.
3:46AM INF scanned ~8262510 bytes (8.26 MB) in 6.62s
3:46AM INF no leaks found
suite: gitleaks took 9s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m7.080000s 0m0.230000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: ./internal/harness/ runs its 10 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/harness	5.770s
suite: browser.internal.harness took 10s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m1.720000s 0m1.310000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 16 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	87.684s
suite: browser.check~1of13 took 97s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.480000s 0m13.550000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	92.134s
suite: browser.check~2of13 took 103s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.360000s 0m13.540000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 15 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	89.539s
suite: browser.check~3of13 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m39.920000s 0m11.130000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 15 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	86.018s
suite: browser.check~4of13 took 97s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.380000s 0m15.490000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	77.129s
suite: browser.check~5of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.510000s 0m13.220000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.308s
suite: browser.check~6of13 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.090000s 0m14.730000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	84.392s
suite: browser.check~7of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.810000s 0m14.960000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	89.149s
suite: browser.check~8of13 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.430000s 0m15.220000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	77.496s
suite: browser.check~9of13 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m29.190000s 0m9.100000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.066s
suite: browser.check~10of13 took 93s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m37.980000s 0m12.050000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.360s
suite: browser.check~11of13 took 92s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.630000s 0m14.840000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.446s
suite: browser.check~12of13 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.410000s 0m16.000000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 17 of 216 browser tests
ok  	amadan.net/rastrillo/fiesta/check	81.008s
suite: browser.check~13of13 took 91s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.770000s 0m10.620000s
--- shard 25 of 48 ---
==> shard 25 of 48: make browser.internal.gallery~1of18
./hack/gotest.sh browser 'internal.gallery~1of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	39.858s
suite: browser.internal.gallery~1of18 took 49s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.130000s 0m3.880000s
--- shard 26 of 48 ---
==> shard 26 of 48: make browser.internal.gallery~2of18
./hack/gotest.sh browser 'internal.gallery~2of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	50.795s
suite: browser.internal.gallery~2of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.150000s 0m2.700000s
--- shard 27 of 48 ---
==> shard 27 of 48: make browser.internal.gallery~3of18
./hack/gotest.sh browser 'internal.gallery~3of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.448s
suite: browser.internal.gallery~3of18 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.390000s 0m4.170000s
--- shard 28 of 48 ---
==> shard 28 of 48: make browser.internal.gallery~4of18
./hack/gotest.sh browser 'internal.gallery~4of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	50.499s
suite: browser.internal.gallery~4of18 took 60s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.980000s 0m2.860000s
--- shard 29 of 48 ---
==> shard 29 of 48: make browser.internal.gallery~5of18
./hack/gotest.sh browser 'internal.gallery~5of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 5 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	59.629s
suite: browser.internal.gallery~5of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.540000s 0m5.190000s
--- shard 30 of 48 ---
==> shard 30 of 48: make browser.internal.gallery~6of18
./hack/gotest.sh browser 'internal.gallery~6of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 6 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	58.181s
suite: browser.internal.gallery~6of18 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.270000s 0m3.990000s
--- shard 31 of 48 ---
==> shard 31 of 48: make browser.internal.gallery~7of18
./hack/gotest.sh browser 'internal.gallery~7of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 7 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	55.364s
suite: browser.internal.gallery~7of18 took 65s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.250000s 0m2.880000s
--- shard 32 of 48 ---
==> shard 32 of 48: make browser.internal.gallery~8of18
./hack/gotest.sh browser 'internal.gallery~8of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 8 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	58.633s
suite: browser.internal.gallery~8of18 took 68s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.100000s 0m4.740000s
--- shard 33 of 48 ---
==> shard 33 of 48: make browser.internal.gallery~9of18
./hack/gotest.sh browser 'internal.gallery~9of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 9 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	66.707s
suite: browser.internal.gallery~9of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m22.980000s 0m7.590000s
--- shard 34 of 48 ---
==> shard 34 of 48: make browser.internal.gallery~10of18
./hack/gotest.sh browser 'internal.gallery~10of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 10 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.887s
suite: browser.internal.gallery~10of18 took 70s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.130000s 0m2.750000s
--- shard 35 of 48 ---
==> shard 35 of 48: make browser.internal.gallery~11of18
./hack/gotest.sh browser 'internal.gallery~11of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 11 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	61.112s
suite: browser.internal.gallery~11of18 took 70s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.260000s 0m2.710000s
--- shard 36 of 48 ---
==> shard 36 of 48: make browser.internal.gallery~12of18
./hack/gotest.sh browser 'internal.gallery~12of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 12 of 18 of ./internal/gallery/ runs 2 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	64.600s
suite: browser.internal.gallery~12of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.640000s 0m4.820000s
--- shard 37 of 48 ---
==> shard 37 of 48: make browser.internal.gallery~13of18
./hack/gotest.sh browser 'internal.gallery~13of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 13 of 18 of ./internal/gallery/ runs 4 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	65.188s
suite: browser.internal.gallery~13of18 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.100000s 0m5.360000s
--- shard 38 of 48 ---
==> shard 38 of 48: make browser.internal.gallery~14of18
./hack/gotest.sh browser 'internal.gallery~14of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 14 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	67.617s
suite: browser.internal.gallery~14of18 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.580000s 0m5.510000s
--- shard 39 of 48 ---
==> shard 39 of 48: make browser.internal.gallery~15of18
./hack/gotest.sh browser 'internal.gallery~15of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 15 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.425s
suite: browser.internal.gallery~15of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.320000s 0m5.510000s
--- shard 40 of 48 ---
==> shard 40 of 48: make browser.internal.gallery~16of18
./hack/gotest.sh browser 'internal.gallery~16of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 16 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.073s
suite: browser.internal.gallery~16of18 took 73s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.660000s 0m4.280000s
--- shard 41 of 48 ---
==> shard 41 of 48: make browser.internal.gallery~17of18
./hack/gotest.sh browser 'internal.gallery~17of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 17 of 18 of ./internal/gallery/ runs 5 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.153s
suite: browser.internal.gallery~17of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.490000s 0m5.820000s
--- shard 42 of 48 ---
==> shard 42 of 48: make browser.internal.gallery~18of18
./hack/gotest.sh browser 'internal.gallery~18of18'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 18 of 18 of ./internal/gallery/ runs 6 of 84 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.845s
suite: browser.internal.gallery~18of18 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.920000s 0m6.330000s
--- shard 43 of 48 ---
==> shard 43 of 48: make browser.internal.gallery.history
./hack/gotest.sh browser 'internal.gallery.history'
gotest: on ip-172-31-31-85, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: ./internal/gallery/history/ runs its 3 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery/history	33.327s
suite: browser.internal.gallery.history took 39s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.250000s 0m2.040000s
--- shard 44 of 48 ---
==> shard 44 of 48: make browser.internal.www~1of4
./hack/gotest.sh browser 'internal.www~1of4'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 1 of 4 of ./internal/www/ runs 4 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	70.074s
suite: browser.internal.www~1of4 took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m31.350000s 0m2.180000s
--- shard 45 of 48 ---
==> shard 45 of 48: make browser.internal.www~2of4
./hack/gotest.sh browser 'internal.www~2of4'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 2 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	66.732s
suite: browser.internal.www~2of4 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.590000s 0m2.710000s
--- shard 46 of 48 ---
==> shard 46 of 48: make browser.internal.www~3of4
./hack/gotest.sh browser 'internal.www~3of4'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 3 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	80.959s
suite: browser.internal.www~3of4 took 89s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m51.850000s 0m2.970000s
--- shard 47 of 48 ---
==> shard 47 of 48: make browser.internal.www~4of4
./hack/gotest.sh browser 'internal.www~4of4'
gotest: on ip-172-31-31-85, driving the harness's own choice (/usr/local/bin/chromium)
gotest: slice 4 of 4 of ./internal/www/ runs 6 of 22 browser tests
ok  	amadan.net/rastrillo/fiesta/internal/www	78.416s
suite: browser.internal.www~4of4 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.840000s 0m2.940000s
diff --git a/hack/test-weights.txt b/hack/test-weights.txt
index 53175b8..7be8d70 100644
--- a/hack/test-weights.txt
+++ b/hack/test-weights.txt
@@ -147,6 +147,7 @@ browser internal/gallery TestThePreviewDefaultIsMonotoneInStageWidth 12
browser internal/gallery TestThePreviewWidgetIsUsableOnAPhone 11
browser internal/gallery TestTheBarsLanguageMenuStaysOnScreen 9
browser internal/gallery TestAFrameStopsLookingForItsNextDocument 8
+browser internal/gallery TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen 7
browser internal/gallery TestTheGalleryHasNoHorizontalScrollAt320 7
browser internal/gallery TestA11yScansTheCodePanels 5
browser internal/gallery TestA11yScansThePhoneViews 5
diff --git a/internal/gallery/heights.go b/internal/gallery/heights.go
index ce3bfa0..bf4de9f 100644
--- a/internal/gallery/heights.go
+++ b/internal/gallery/heights.go
@@ -19,7 +19,10 @@ import (
// calendar or clock. The drives open each in turn in a box made tall
// enough that a menu's dvh cap is not the frame, and the number here is
// the larger of the two readings, so a box sized for the closed state
-// is a short box. Rastrillo reserves the same room by hand.
+// is a short box. Rastrillo reserves the same room by hand. The one
+// exception is a frame in previewRestHeights, whose open menus need far
+// more than its page: there the box rests at the closed reading and
+// view.js grows it to this one while a menu is open.
//
// Seeded once by the widget task to prove the measurement; calibrated
// once the shell, the frame padding, the stage and the demo were all
@@ -365,6 +368,20 @@ var previewReserved = map[string]int{
"modal/default": 620,
}
+// restHeights is a box's Desktop and Mobile heights while nothing in
+// its frame is open.
+type restHeights struct{ Desktop, Mobile int }
+
+// previewRestHeights is for a frame whose open menu needs far more room
+// than its page: the column's footer menus open downward under the
+// page, so a box sized for them open is mostly empty at rest. The main
+// tables keep the open height; view.js shows these while no menu in
+// the frame is open and the open height while one is. With scripts
+// off the box keeps the open height, since nothing could grow it.
+var previewRestHeights = map[string]restHeights{
+ "layout-column/default": {Desktop: 650, Mobile: 1110},
+}
+
// heightOf is one key's Desktop height: the measured table's, or the
// default, raised to its reservation where it has one.
func heightOf(key string) int {
@@ -384,6 +401,17 @@ func heightOf(key string) int {
// one column either way, and the exceptions are the ones whose whole
// shape is a row that becomes a column.
func previewStyle(locale, key string) template.CSS {
+ h, m := previewHeightsFor(locale, key)
+ css := fmt.Sprintf("--ds-h: %dpx; --ds-hm: %dpx", h, m)
+ if r, ok := previewRestHeights[key]; ok {
+ css += fmt.Sprintf("; --ds-hr: %dpx; --ds-hmr: %dpx", r.Desktop, r.Mobile)
+ }
+ return template.CSS(css)
+}
+
+// previewHeightsFor is one widget's Desktop and Mobile heights in
+// locale, as previewStyle writes them.
+func previewHeightsFor(locale, key string) (desktop, mobile int) {
h := heightOf(key)
m, ok := previewMobileHeights[key]
if !ok {
@@ -392,5 +420,5 @@ func previewStyle(locale, key string) template.CSS {
if l, ok := previewMobileLocaleHeights[locale][key]; ok && l > m {
m = l
}
- return template.CSS(fmt.Sprintf("--ds-h: %dpx; --ds-hm: %dpx", h, m))
+ return h, m
}
diff --git a/internal/gallery/heights_browser_test.go b/internal/gallery/heights_browser_test.go
index efcd3dd..de994a0 100644
--- a/internal/gallery/heights_browser_test.go
+++ b/internal/gallery/heights_browser_test.go
@@ -14,6 +14,7 @@ import (
"github.com/chromedp/chromedp"
+ "amadan.net/rastrillo/fiesta/internal/gallery/galleryrig"
"amadan.net/rastrillo/fiesta/internal/harness"
)
@@ -78,7 +79,10 @@ const frameKey = `const frameKey = f => {
// its document needs in its own pixels, what its box gives it, which is
// --ds-h or --ds-hm resolved, since the frame takes its height back off
// the box in virtual pixels, and whether it frames a sidebar shell,
-// whose rail is 100dvh tall.
+// whose rail is 100dvh tall. A widget view.js has marked at rest
+// (data-ds-rest) gives its rest height as Box, and is unmarked for a
+// moment to read the height it opens to as Open; any other's Open is
+// its Box.
const measureHeights = `(() => {
` + frameKey + `
const out = {};
@@ -89,13 +93,25 @@ const measureHeights = `(() => {
const need = d && d.body ? Math.ceil(Math.max(d.body.getBoundingClientRect().height, d.body.scrollHeight)) : -1;
const shell = !!(d && d.querySelector("[rst-shell-sidebar]"));
const viewport = !!(d && d.querySelector("[rst-shell-sidebar], [rst-shell-topbar], [rst-shell-console], [rst-stage]"));
- out[key] = {Need: need, Box: Math.round(parseFloat(getComputedStyle(f).height)), Shell: shell, Viewport: viewport};
+ const view = f.closest(".ds-view"), rest = view.hasAttribute("data-ds-rest"), box = Math.round(parseFloat(getComputedStyle(f).height));
+ let open = box;
+ if (rest) {
+ view.removeAttribute("data-ds-rest");
+ open = Math.round(parseFloat(getComputedStyle(f).height));
+ view.setAttribute("data-ds-rest", "");
+ }
+ out[key] = {Need: need, Box: box, Open: open, Rest: rest, Shell: shell, Viewport: viewport};
}
return JSON.stringify(out);
})()`
type heightReading struct {
Need, Box int
+ // Open is the box while something in the frame is open: Box, but
+ // for a widget with a rest height (previewRestHeights), which Rest
+ // says view.js had marked at rest when Box was read.
+ Open int
+ Rest bool
// Shell is a frame holding a sidebar shell, whose rail is 100dvh;
// Viewport any document as tall as its frame (a shell, the stage),
// which reads its own box when its content fits.
@@ -115,6 +131,11 @@ func readHeights(t *testing.T, ctx context.Context, where string) map[string]hei
if r.Need < 0 {
t.Fatalf("%s %s: the frame has no document in it", where, key)
}
+ // A rest height the drive never sees in force is a box nobody
+ // measured: the rest readings would be of the open box.
+ if _, rest := previewRestHeights[key]; rest != r.Rest {
+ t.Fatalf("%s %s: view.js has the widget at rest %v, and previewRestHeights says it has a rest height %v; the readings would be of the wrong box", where, key, r.Rest, rest)
+ }
if strings.HasPrefix(key, "?") {
t.Fatalf("%s: a frame's src names no document under frames/ and is not the demo", where)
}
@@ -282,7 +303,7 @@ func shortfall(t *testing.T, where, key string, r heightReading, grace int) bool
}
return false
}
- msg := fmt.Sprintf("%s %s: its document needs %dpx and its frame is %dpx; raise its height (previewHeights for Desktop, previewMobileHeights or previewMobileLocaleHeights for Mobile) to at least %d", where, key, r.Need, r.Box, r.Need+20)
+ msg := fmt.Sprintf("%s %s: its document needs %dpx and its frame is %dpx; raise its height (previewHeights for Desktop, previewMobileHeights or previewMobileLocaleHeights for Mobile, or its previewRestHeights entry if it has one and nothing is open) to at least %d", where, key, r.Need, r.Box, r.Need+20)
if heightsEnforced {
t.Error(msg)
} else {
@@ -522,6 +543,10 @@ func wideHeights(t *testing.T, k, n int) {
ctx, cancel := context.WithTimeout(rig.Context(), deadline)
defer cancel()
desk, mob := map[string]map[string]int{}, map[string]map[string]int{}
+ // What a widget with a rest height needs at rest, over the locales
+ // read: its previewRestHeights entry, which the table below, the
+ // open heights, does not carry.
+ restDesk, restMob := map[string]int{}, map[string]int{}
measured, opened, short := 0, 0, 0
readDemo := false
keep := func(m map[string]int, key string, n int) {
@@ -552,6 +577,9 @@ func wideHeights(t *testing.T, k, n int) {
for key, r := range desktop {
measured++
keep(desk[locale], key, r.Need)
+ if r.Rest {
+ keep(restDesk, key, r.Need)
+ }
fits(deskFit, key, r)
if shortfall(t, where+" Desktop", key, r, 48) {
short++
@@ -561,7 +589,7 @@ func wideHeights(t *testing.T, k, n int) {
opened++
keep(desk[locale], key, o.Need)
deskFit[key] = false
- if openShortfall(t, where+" Desktop", key, o, desktop[key].Box) {
+ if openShortfall(t, where+" Desktop", key, o, desktop[key].Open) {
short++
}
}
@@ -570,6 +598,9 @@ func wideHeights(t *testing.T, k, n int) {
mobile := readHeights(t, ctx, where+" Mobile")
for key, r := range mobile {
keep(mob[locale], key, r.Need)
+ if r.Rest {
+ keep(restMob, key, r.Need)
+ }
fits(mobFit, key, r)
if shortfall(t, where+" Mobile", key, r, 48) {
short++
@@ -578,7 +609,7 @@ func wideHeights(t *testing.T, k, n int) {
for key, o := range readOpen(t, ctx, where+" Mobile", "--ds-hm") {
keep(mob[locale], key, o.Need)
mobFit[key] = false
- if openShortfall(t, where+" Mobile", key, o, mobile[key].Box) {
+ if openShortfall(t, where+" Mobile", key, o, mobile[key].Open) {
short++
}
}
@@ -600,6 +631,9 @@ func wideHeights(t *testing.T, k, n int) {
t.Logf("part %d: %d pages, %d frames measured and %d opened over %d locale(s); %d short (enforced: %v; the Overview's demo read here: %v)", k, len(units), measured, opened, len(desk), short, heightsEnforced, readDemo)
if sweepFull() {
printTable(t, desk, mob, deskFit, mobFit)
+ for key, n := range restDesk {
+ t.Logf("previewRestHeights as measured at 1500px (the 320px drive may raise Mobile): %q: {Desktop: %d, Mobile: %d},", key, (n+20+9)/10*10, (restMob[key]+20+9)/10*10)
+ }
}
}
@@ -699,7 +733,7 @@ func narrowHeights(t *testing.T, k, n int) {
t.Fatalf("%s: frames %v are not at the stage's narrow width; the readings would be of the 390px frame", where, wide)
}
got := readHeights(t, ctx, where)
- need := map[string]int{}
+ need, openNeed := map[string]int{}, map[string]int{}
pageShells := 0
for key, r := range got {
measured++
@@ -717,10 +751,8 @@ func narrowHeights(t *testing.T, k, n int) {
}
for key, o := range readOpen(t, ctx, where, "--ds-hm") {
opened++
- if o.Need > need[key] {
- need[key] = o.Need
- }
- if openShortfall(t, where, key, o, got[key].Box) {
+ openNeed[key] = o.Need
+ if openShortfall(t, where, key, o, got[key].Open) {
short++
}
}
@@ -729,8 +761,16 @@ func narrowHeights(t *testing.T, k, n int) {
if got[key].Shell {
grace = 48
}
- if n > got[key].Box+grace {
- raise = append(raise, fmt.Sprintf("%s %q: %d,", locale, key, (n+20+9)/10*10))
+ // A widget without a rest height holds both in one box.
+ rest, open := n, max(n, openNeed[key])
+ if got[key].Rest {
+ open = openNeed[key]
+ if rest > got[key].Box+grace {
+ raise = append(raise, fmt.Sprintf("%s %q: rest Mobile %d (previewRestHeights),", locale, key, (rest+20+9)/10*10))
+ }
+ }
+ if open > got[key].Open+grace {
+ raise = append(raise, fmt.Sprintf("%s %q: %d,", locale, key, (open+20+9)/10*10))
}
}
if file == "button.html" && locale == "en" {
@@ -746,3 +786,85 @@ func narrowHeights(t *testing.T, k, n int) {
t.Logf("Mobile entries to raise, as locale key: height:\n\t%s", strings.Join(raise, "\n\t"))
}
}
+
+// columnFrame reads the column page's one frame: its height in the
+// frame's own pixels, which is the box's --ds-h or --ds-hm resolved,
+// and whether its language and scheme menus are open, after two frames
+// and a beat, so a resize view.js makes on a toggle has landed. act,
+// run in the framed document first, is "" or a click to make.
+func columnFrame(t *testing.T, ctx context.Context, where, act string) (height float64, open []bool) {
+ t.Helper()
+ var got struct {
+ Height float64
+ Open []bool
+ }
+ at(t, ctx, `(async () => {
+ const f = document.querySelector(".ds-view__frame"), d = f.contentDocument, w = d.defaultView;
+ `+act+`
+ for (const win of [window, w]) await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(() => setTimeout(r, 50))));
+ return JSON.stringify({Height: parseFloat(getComputedStyle(f).height), Open: [...d.querySelectorAll("details[rst-dropdown]")].map(m => m.open)});
+ })()`, &got, awaitPromise)
+ if len(got.Open) != 2 {
+ t.Fatalf("%s: the framed column has %d menus, want its footer's two", where, len(got.Open))
+ }
+ return got.Height, got.Open
+}
+
+// TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen: the column's box is
+// its page's height at rest and the open height while a footer menu
+// is open, on Desktop and on Mobile. Opening the scheme menu while the
+// language menu is open closes the language menu (the two share a
+// name), and the box stays open; closing the last one brings it back.
+// With scripts off nothing could grow the box, so it keeps the open
+// height, and a menu opened there is still all in view.
+func TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen(t *testing.T) {
+ key := "layout-column/" + mustDefault(t, "layout-column")
+ rest, ok := previewRestHeights[key]
+ if !ok {
+ t.Fatalf("%s has no rest height to switch from", key)
+ }
+ openDesktop, openMobile := previewHeightsFor("en", key)
+ rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
+ ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
+ defer cancel()
+ const language, scheme = `d.querySelector("[rst-locale] > summary").click();`, `d.querySelector("[rst-scheme] > summary").click();`
+ want := func(where string, got, wantH float64, open []bool, wantOpen ...bool) {
+ t.Helper()
+ if fmt.Sprint(open) != fmt.Sprint(wantOpen) {
+ t.Fatalf("%s: the menus' open states are %v, want %v; the step did not do what it says", where, open, wantOpen)
+ }
+ if got < wantH-1 || got > wantH+1 {
+ t.Errorf("%s: the frame is %.0fpx tall, want %.0f", where, got, wantH)
+ }
+ }
+ openPage(t, ctx, "en/layout-column.html", pageURL(rig, "en", "layout-column.html"), 1500, 1000)
+ eagerly(t, ctx, "en/layout-column.html")
+ for _, mode := range []struct {
+ name string
+ rest, open int
+ }{{"Desktop", rest.Desktop, openDesktop}, {"Mobile", rest.Mobile, openMobile}} {
+ if mode.name == "Mobile" {
+ clickAll(t, ctx, "en/layout-column.html", clickedMobile, "Mobile")
+ mobileSettle(t, ctx, "en/layout-column.html")
+ }
+ where := "en/layout-column.html " + mode.name
+ h, open := columnFrame(t, ctx, where, "")
+ want(where+" at rest", h, float64(mode.rest), open, false, false)
+ h, open = columnFrame(t, ctx, where, language)
+ want(where+" with Language open", h, float64(mode.open), open, true, false)
+ h, open = columnFrame(t, ctx, where, scheme)
+ want(where+" with Colour scheme opened over Language", h, float64(mode.open), open, false, true)
+ h, open = columnFrame(t, ctx, where, scheme)
+ want(where+" with both closed again", h, float64(mode.rest), open, false, false)
+ }
+
+ off := galleryrig.NoScripts(t, ctx)
+ mustRun(t, off, chromedp.EmulateViewport(1500, 1000), chromedp.Navigate(pageURL(rig, "en", "layout-column.html")), chromedp.WaitReady(".ds-view__frame", chromedp.ByQuery))
+ galleryrig.RequireScriptsOff(t, off, "en/layout-column.html, scripts off")
+ var h float64
+ until(t, off, "en/layout-column.html, scripts off", `!!document.querySelector(".ds-view__frame").contentDocument?.querySelector("[rst-page-foot]")`)
+ at(t, off, `JSON.stringify(parseFloat(getComputedStyle(document.querySelector(".ds-view__frame")).height))`, &h)
+ if h < float64(openDesktop)-1 || h > float64(openDesktop)+1 {
+ t.Errorf("en/layout-column.html, scripts off: the frame is %.0fpx tall, want the open height %d, since nothing can grow it when a menu opens", h, openDesktop)
+ }
+}
diff --git a/internal/gallery/heights_test.go b/internal/gallery/heights_test.go
new file mode 100644
index 0000000..0972cb5
--- /dev/null
+++ b/internal/gallery/heights_test.go
@@ -0,0 +1,45 @@
+package gallery
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+// TestTheColumnPreviewRestsShorterThanItOpens: the column's footer
+// menus open downward under the page, and a box that always held them
+// open showed about 300px of nothing under the page at rest. So the
+// column has a rest height, and every rest height is written beside
+// the open one for view.js to switch to: under the open height on
+// Desktop and on Mobile in every locale, since a rest height at or
+// over the open one could only hide a short open box. A key without
+// one carries no rest height at all, so view.js leaves it alone.
+func TestTheColumnPreviewRestsShorterThanItOpens(t *testing.T) {
+ column := "layout-column/" + mustEntry(t, "layout-column").Default
+ if _, ok := previewRestHeights[column]; !ok {
+ t.Errorf("%s has no rest height; its box holds the open footer menus at rest", column)
+ }
+ m := manifest(t)
+ for key, rest := range previewRestHeights {
+ name, state, _ := strings.Cut(key, "/")
+ if e, ok := m.Entry(name); !ok || !hasState(e, state) {
+ t.Errorf("previewRestHeights[%q] names no example state", key)
+ continue
+ }
+ for _, locale := range Locales {
+ open, openMobile := previewHeightsFor(locale, key)
+ if rest.Desktop >= open || rest.Mobile >= openMobile {
+ t.Errorf("%s in %s rests at %d, %d and opens at %d, %d; the rest height must be under the open one", key, locale, rest.Desktop, rest.Mobile, open, openMobile)
+ }
+ want := fmt.Sprintf("--ds-hr: %dpx; --ds-hmr: %dpx", rest.Desktop, rest.Mobile)
+ if got := string(previewStyle(locale, key)); !strings.Contains(got, want) {
+ t.Errorf("%s in %s: the widget's style is %q, want it to carry %q", key, locale, got, want)
+ }
+ }
+ }
+ for _, key := range []string{"button/default", "dropdown/" + mustEntry(t, "dropdown").Default} {
+ if got := string(previewStyle("en", key)); strings.Contains(got, "--ds-hr") {
+ t.Errorf("%s has no rest height and its style %q carries one", key, got)
+ }
+ }
+}
diff --git a/internal/gallery/view.css b/internal/gallery/view.css
index 5b84651..151a09f 100644
--- a/internal/gallery/view.css
+++ b/internal/gallery/view.css
@@ -246,6 +246,14 @@ ds-s { color: var(--rst-tone-warning-fg); }
}
/* An explicit Mobile beats the width, at either class and any size. */
.ds-view:has(.ds-view__tab--m input:checked) .ds-view__box { --ds-h: var(--ds-hm); --ds-w: var(--ds-wm); }
+/* A widget with a rest height (--ds-hr and --ds-hmr, written by
+ previewStyle for a frame whose menus open far below its page) is
+ marked data-ds-rest by view.js while no menu in its frame is open.
+ The stage then hands the box the rest heights under the names its
+ Desktop and Mobile rules read, so either rendering rests short.
+ Unmarked, as with scripts off, the box keeps the open height and a
+ menu opened in it is in view. */
+.ds-view[data-ds-rest] > .ds-view__stage { --ds-h: var(--ds-hr); --ds-hm: var(--ds-hmr); }
/* HTML replaces the frame with its panel, and only then: the radios
are one group, so one is checked at most. */
.ds-view__code { display: none; margin-block-start: 0; }
diff --git a/internal/gallery/view.js b/internal/gallery/view.js
index aff62b5..c24f717 100644
--- a/internal/gallery/view.js
+++ b/internal/gallery/view.js
@@ -115,6 +115,7 @@
}
if (!doc || doc === armed.doc || doc.URL === "about:blank") return false;
armed.doc = doc;
+ watchRest(frame, doc);
doc.defaultView.addEventListener("pagehide", function (event) {
if (event.persisted) return;
frame.removeAttribute("data-gallery-painted");
@@ -161,7 +162,9 @@
// the frames are lazy, so nothing looks for them before then. load
// stays the fallback, for an engine that does not reuse the window.
function watch(frame) {
- var armed = { doc: null, timer: 0 };
+ var armed = { doc: null, timer: 0 }, view = restView(frame);
+ // Nothing is open in a frame that has not loaded.
+ if (view) view.setAttribute("data-ds-rest", "");
try {
frame.contentWindow.addEventListener("DOMContentLoaded", function () {
arm(frame, armed);
@@ -179,6 +182,51 @@
arm(frame, armed);
}
+ // ── Rest heights ────────────────────────────────────────────────────
+ //
+ // A frame whose menus open far below its page (the column's footer
+ // row) has two heights, the open one in --ds-h and --ds-hm and the
+ // rest one in --ds-hr and --ds-hmr. Sized for its menus open, its box
+ // was mostly empty at rest. While no menu in the frame is open its
+ // widget is marked data-ds-rest, and view.css gives the box the rest
+ // heights; unmarked, as with scripts off, the box keeps the open
+ // height, so a menu a reader opens is in view either way.
+ //
+ // A click on a closed menu's summary unmarks the widget at once,
+ // before the menu opens, so the frame is already tall when the panel
+ // is placed: placed in the short frame, an anchored panel opens up
+ // over the page, and stays there once the frame grows. toggle then
+ // settles the mark from what is really open, which also covers a menu
+ // opened from the keyboard or by a script, one closed by its sibling
+ // opening, and a click whose menu did not open after all.
+ var MENU_OPEN = "details[rst-dropdown][open], details.rst-dropdown[open]";
+
+ function restView(frame) {
+ var view = frame.closest(".ds-view");
+ return view && view.style.getPropertyValue("--ds-hr") ? view : null;
+ }
+
+ function settleRest(view, doc) {
+ view.toggleAttribute("data-ds-rest", !doc.querySelector(MENU_OPEN));
+ }
+
+ function watchRest(frame, doc) {
+ var view = restView(frame);
+ if (!view) return;
+ settleRest(view, doc);
+ doc.addEventListener("toggle", function () {
+ settleRest(view, doc);
+ }, true);
+ doc.addEventListener("click", function (event) {
+ var summary = event.target.closest && event.target.closest("summary"), menu = summary && summary.parentElement;
+ if (!menu || menu.open || !menu.matches("details[rst-dropdown], details.rst-dropdown")) return;
+ view.removeAttribute("data-ds-rest");
+ setTimeout(function () {
+ settleRest(view, doc);
+ }, 100);
+ }, true);
+ }
+
// fiesta.js dispatches rst-scheme from the form after it has written
// the attribute, so the detail and the root agree; the detail is
// read all the same, so this file never depends on the order.