rastrillo / fiesta Public

Keep a side nav link reached with Tab whole in the row, ring included

In a narrow side the links are a row that scrolls, and a link reached
with Tab must be wholly in view with its ring. Two things kept it from
being so. Chromium does not scroll a focused element that is already
partly in view, so Tab to the second of four long links left it half
under the row's edge, cut off, and Shift+Tab back did the same at the
other end. And the ring was drawn 4px outside the link, where the row
clips it wherever a link meets its edge.

scroll-padding-inline sets the part of the row Chromium counts as in
view. At 49% on each side that part is a sliver at the centre, so a
focused link that does not cross the centre is scrolled to it, whole.
Paddings of 8, 16 and 40px were tried first and each still left a
link part out of view on one leg. In the row the ring is drawn inside
the link (outline-offset: -2px), as the shell's index rows draw
theirs.

TestKeyboardFocusShowsTheWholeRingInTheRow tabs through four long
links and Shift+Tabs back, in a 300px panel at 1280 and on a 320px
page, left to right and right to left, and requires each focused
link's whole ring inside the row's scrollport, and the ring inside the
link. Red before the fix on both counts: 12 rings cut off (the second
and fourth link going forward, the second coming back, in every leg)
and 28 rings outside their link. Mutations, each red by name: the
scroll padding dropped (the rings cut off again), the inset dropped
(4px outside the link).

A side answers to its own width, so it cannot take that width from its
content: as a flex item sized by its content it is zero wide.
TestASideInAFlexRowIsGivenItsWidth holds the composition that works,
flex: 1 1 0 in a bounded row beside a 200px column (800px and a column
of links in a 1000px row, 300px and one row of links in a 500px row),
and holds the unsized case to collapsing, so the advice pages are
given stays true. It passed on the CSS as it was; it is red with the
side given flex: 0 1 auto instead (zero wide in both rows) and with
container-type dropped (the side sized by its content, the narrow row
four lines tall). The note telling readers this is in copy review and
lands separately.

Size, gzip level 6 (Task 8 now +312 of its 360):
  fiesta.css        23,699 -> 23,724 (+25)
  fiesta-plain.css  22,979 -> 23,005 (+26)
  fiesta-signal.css 23,091 -> 23,118 (+27)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev af340e7fcc3abd7ae23210470c1b15f138c8674a parent 5aea74d
2 files changed, +151 −2
  • check/side_browser_test.go +140 −0
  • css/tokens.css +11 −2

CI passed — run details

CI log
=== gofmt ===
=== no-rastrillo ===
go test -run '^(TestFiestaNeverImportsRastrillo|TestThePublicPackagesLinkNoBrowser)$' -count=1 .
ok  	amadan.net/rastrillo/fiesta	0.215s
=== size ===
go test ./internal/build -run '^TestTheOneLinkIsUnder25KiBGzipped$' -count=1 -v
=== RUN   TestTheOneLinkIsUnder25KiBGzipped
    build_test.go:138: fiesta-plain.css: 157686 bytes, 23005 bytes gzipped (22.5 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta-signal.css: 157781 bytes, 23118 bytes gzipped (22.6 KiB of the 25.0 KiB limit)
    build_test.go:138: fiesta.css: 160600 bytes, 23724 bytes gzipped (23.2 KiB of the 25.0 KiB limit)
--- PASS: TestTheOneLinkIsUnder25KiBGzipped (0.02s)
PASS
ok  	amadan.net/rastrillo/fiesta/internal/build	0.025s
=== history ===
history: 28 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	2.533s
ok  	amadan.net/rastrillo/fiesta/check	7.685s
ok  	amadan.net/rastrillo/fiesta/example	0.182s
?   	amadan.net/rastrillo/fiesta/hack/seed/english	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/axe	0.133s
ok  	amadan.net/rastrillo/fiesta/internal/build	1.056s
ok  	amadan.net/rastrillo/fiesta/internal/codeview	0.012s
?   	amadan.net/rastrillo/fiesta/internal/harness	[no test files]
ok  	amadan.net/rastrillo/fiesta/internal/markup	0.007s
ok  	amadan.net/rastrillo/fiesta/internal/nodetest	0.401s
ok  	amadan.net/rastrillo/fiesta/internal/site	2.622s
ok  	amadan.net/rastrillo/fiesta/internal/tokens	0.009s
suite: root took 24s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m11.650000s 0m2.060000s
--- 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 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	64.113s
suite: test.internal.gallery~1of3 took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m50.900000s 0m1.390000s
--- 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 36 of 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	68.307s
suite: test.internal.gallery~2of3 took 83s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m54.040000s 0m1.470000s
--- 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 108 test tests
ok  	amadan.net/rastrillo/fiesta/internal/gallery	63.637s
suite: test.internal.gallery~3of3 took 78s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.150000s 0m1.280000s
--- 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	47.991s
suite: test.cmd.fiesta~1of2 took 62s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m33.670000s 0m2.130000s
--- 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	50.437s
suite: test.cmd.fiesta~2of2 took 64s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m36.160000s 0m2.480000s
--- 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	62.652s
suite: test.internal.www~1of2 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.700000s 0m1.590000s
--- 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	60.345s
suite: test.internal.www~2of2 took 75s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.850000s 0m1.560000s
--- 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 17s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.490000s 0m0.930000s
--- 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 14s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m3.070000s 0m0.320000s
--- 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' .
7:02AM INF Unknown SCM platform. Use --platform to include links in findings. host=amadan.net
7:03AM INF 28 commits scanned.
7:03AM INF scanned ~8386001 bytes (8.39 MB) in 15.1s
7:03AM INF no leaks found
suite: gitleaks took 17s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m6.860000s 0m0.260000s
--- shard 11 of 48 ---
==> shard 11 of 48: make browser.internal.harness
./hack/gotest.sh browser 'internal.harness'
gotest: on ip-172-31-22-133, 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	4.729s
suite: browser.internal.harness took 20s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m1.700000s 0m1.300000s
--- shard 12 of 48 ---
==> shard 12 of 48: make browser.check~1of13
./hack/gotest.sh browser 'check~1of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 1 of 13 of ./check/ runs 16 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	92.571s
suite: browser.check~1of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m49.710000s 0m14.470000s
--- shard 13 of 48 ---
==> shard 13 of 48: make browser.check~2of13
./hack/gotest.sh browser 'check~2of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 2 of 13 of ./check/ runs 16 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	92.451s
suite: browser.check~2of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m52.240000s 0m13.970000s
--- shard 14 of 48 ---
==> shard 14 of 48: make browser.check~3of13
./hack/gotest.sh browser 'check~3of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 3 of 13 of ./check/ runs 16 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	90.729s
suite: browser.check~3of13 took 108s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m42.750000s 0m11.700000s
--- shard 15 of 48 ---
==> shard 15 of 48: make browser.check~4of13
./hack/gotest.sh browser 'check~4of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 4 of 13 of ./check/ runs 16 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	91.534s
suite: browser.check~4of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m54.090000s 0m16.000000s
--- shard 16 of 48 ---
==> shard 16 of 48: make browser.check~5of13
./hack/gotest.sh browser 'check~5of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 5 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	80.941s
suite: browser.check~5of13 took 99s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.740000s 0m13.860000s
--- shard 17 of 48 ---
==> shard 17 of 48: make browser.check~6of13
./hack/gotest.sh browser 'check~6of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 6 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	77.552s
suite: browser.check~6of13 took 95s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m44.350000s 0m14.430000s
--- shard 18 of 48 ---
==> shard 18 of 48: make browser.check~7of13
./hack/gotest.sh browser 'check~7of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 7 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	87.008s
suite: browser.check~7of13 took 105s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m45.370000s 0m14.520000s
--- shard 19 of 48 ---
==> shard 19 of 48: make browser.check~8of13
./hack/gotest.sh browser 'check~8of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 8 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	92.086s
suite: browser.check~8of13 took 110s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m48.070000s 0m16.490000s
--- shard 20 of 48 ---
==> shard 20 of 48: make browser.check~9of13
./hack/gotest.sh browser 'check~9of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 9 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	76.599s
suite: browser.check~9of13 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m30.570000s 0m9.540000s
--- shard 21 of 48 ---
==> shard 21 of 48: make browser.check~10of13
./hack/gotest.sh browser 'check~10of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 10 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	83.537s
suite: browser.check~10of13 took 101s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m40.820000s 0m13.190000s
--- shard 22 of 48 ---
==> shard 22 of 48: make browser.check~11of13
./hack/gotest.sh browser 'check~11of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 11 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	84.336s
suite: browser.check~11of13 took 102s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m43.730000s 0m14.740000s
--- shard 23 of 48 ---
==> shard 23 of 48: make browser.check~12of13
./hack/gotest.sh browser 'check~12of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 12 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	85.243s
suite: browser.check~12of13 took 103s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m47.190000s 0m16.010000s
--- shard 24 of 48 ---
==> shard 24 of 48: make browser.check~13of13
./hack/gotest.sh browser 'check~13of13'
gotest: on ip-172-31-22-133, driving /opt/amadan-browser/chromium-1234/chrome-linux/chrome
gotest: slice 13 of 13 of ./check/ runs 18 of 226 browser tests
ok  	amadan.net/rastrillo/fiesta/check	82.657s
suite: browser.check~13of13 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m38.740000s 0m11.460000s
--- 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-22-133, 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	40.201s
suite: browser.internal.gallery~1of18 took 57s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.170000s 0m3.950000s
--- 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-22-133, 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	47.464s
suite: browser.internal.gallery~2of18 took 65s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m13.940000s 0m2.550000s
--- 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-22-133, 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.860s
suite: browser.internal.gallery~3of18 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.310000s 0m4.160000s
--- 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-22-133, 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	51.628s
suite: browser.internal.gallery~4of18 took 69s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.580000s 0m2.970000s
--- 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-22-133, 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	61.493s
suite: browser.internal.gallery~5of18 took 79s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.290000s 0m5.560000s
--- 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-22-133, 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	57.110s
suite: browser.internal.gallery~6of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m16.400000s 0m4.080000s
--- 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-22-133, 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	56.718s
suite: browser.internal.gallery~7of18 took 74s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.270000s 0m2.980000s
--- 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-22-133, 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	59.185s
suite: browser.internal.gallery~8of18 took 77s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m18.560000s 0m4.820000s
--- 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-22-133, 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	67.373s
suite: browser.internal.gallery~9of18 took 84s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m23.630000s 0m7.660000s
--- 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-22-133, 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	64.137s
suite: browser.internal.gallery~10of18 took 81s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.310000s 0m2.840000s
--- 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-22-133, 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	64.767s
suite: browser.internal.gallery~11of18 took 82s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m14.800000s 0m3.000000s
--- 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-22-133, 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	66.625s
suite: browser.internal.gallery~12of18 took 84s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.290000s 0m5.160000s
--- 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-22-133, 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.590s
suite: browser.internal.gallery~13of18 took 83s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.410000s 0m5.540000s
--- 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-22-133, 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	68.719s
suite: browser.internal.gallery~14of18 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.880000s 0m5.640000s
--- 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-22-133, 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	69.446s
suite: browser.internal.gallery~15of18 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.090000s 0m5.720000s
--- 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-22-133, 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	66.127s
suite: browser.internal.gallery~16of18 took 83s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m17.360000s 0m4.580000s
--- 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-22-133, 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	69.498s
suite: browser.internal.gallery~17of18 took 87s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m19.600000s 0m5.960000s
--- 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-22-133, 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	71.018s
suite: browser.internal.gallery~18of18 took 88s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m20.970000s 0m6.670000s
--- 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-22-133, 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	32.916s
suite: browser.internal.gallery.history took 48s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m21.800000s 0m2.120000s
--- 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-22-133, 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.071s
suite: browser.internal.www~1of4 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m32.130000s 0m2.100000s
--- 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-22-133, 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	69.778s
suite: browser.internal.www~2of4 took 86s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.730000s 0m2.790000s
--- 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-22-133, 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	83.284s
suite: browser.internal.www~3of4 took 100s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m53.400000s 0m3.070000s
--- 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-22-133, 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.096s
suite: browser.internal.www~4of4 took 94s; CPU, user then system, of this shell and then of the role:
0m0.000000s 0m0.000000s
0m46.840000s 0m3.080000s
diff --git a/check/side_browser_test.go b/check/side_browser_test.go
index 54a4a9e..a9c3e6b 100644
--- a/check/side_browser_test.go
+++ b/check/side_browser_test.go
@@ -19,6 +19,7 @@ import (
"time"
"github.com/chromedp/cdproto/emulation"
+ "github.com/chromedp/cdproto/input"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/kb"
@@ -359,3 +360,142 @@ func TestTheSideNavLinksAreMenuRows(t *testing.T) {
t.Errorf("a link focused from the keyboard has outline %s %s, want solid 2px though no rst-page is around it", ring.Style, ring.Width)
}
}
+
+// ringJS reads the focused element's whole focus ring (its border box
+// grown by the outline's offset and width, which a negative offset
+// shrinks) against the nav's scrollport (its padding box), where the
+// row's overflow clips anything outside.
+const ringJS = `(() => {
+ const a = document.activeElement, nav = a.closest("[rst-side-nav]");
+ if (!nav) return JSON.stringify({Href: a.getAttribute("href") || a.tagName});
+ const s = getComputedStyle(a), ns = getComputedStyle(nav), r = a.getBoundingClientRect(), n = nav.getBoundingClientRect();
+ const px = (v) => parseFloat(v) || 0, grow = s.outlineStyle === "none" ? 0 : px(s.outlineOffset) + px(s.outlineWidth);
+ return JSON.stringify({Href: a.getAttribute("href"), Style: s.outlineStyle, Grow: grow,
+ Ring: {Left: r.left - grow, Right: r.right + grow, Top: r.top - grow, Bottom: r.bottom + grow},
+ Port: {Left: n.left + px(ns.borderLeftWidth), Right: n.right - px(ns.borderRightWidth), Top: n.top + px(ns.borderTopWidth), Bottom: n.bottom - px(ns.borderBottomWidth)}});
+})()`
+
+// TestKeyboardFocusShowsTheWholeRingInTheRow: in a narrow panel whose
+// row of links is wider than the panel, Tab reaches each link in turn,
+// links that start out of view included, and Shift+Tab walks back to
+// the first after the row has scrolled to its end; each focused link
+// and its whole focus ring are inside the row's visible part, left to
+// right and right to left; and the ring is drawn inside the link, since
+// the row's edge cuts anything outside a link that meets it.
+func TestKeyboardFocusShowsTheWholeRingInTheRow(t *testing.T) {
+ _, doc := sideNavExample(t)
+ long := inPanel(t, longLabels(t, doc), "300px")
+ pages := map[string]string{"/": long, "/rtl": inArabic(t, long), "/page": longLabels(t, doc), "/page-rtl": inArabic(t, longLabels(t, doc))}
+ rig := sizingRig(t, false, pages)
+ ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
+ defer cancel()
+ for _, path := range slices.Sorted(maps.Keys(pages)) {
+ width := int64(1280)
+ if strings.HasPrefix(path, "/page") {
+ width = 320
+ }
+ g := readSide(t, ctx, rig.Origin, path, width)
+ if g.NavScroll <= g.NavClient {
+ t.Fatalf("%s: the row is %.0fpx in %.0fpx; with nothing out of view this leg proves nothing", path, g.NavScroll, g.NavClient)
+ }
+ var hrefs []string
+ at(t, ctx, `JSON.stringify([...document.querySelectorAll("[rst-side-nav] a")].map(a => a.getAttribute("href")))`, &hrefs)
+ var hidden int
+ at(t, ctx, `(() => { const n = document.querySelector("[rst-side-nav]").getBoundingClientRect();
+ return JSON.stringify([...document.querySelectorAll("[rst-side-nav] a")].filter(a => { const r = a.getBoundingClientRect(); return r.right > n.right + 0.5 || r.left < n.left - 0.5; }).length); })()`, &hidden)
+ if hidden == 0 {
+ t.Fatalf("%s: every link starts in view; this leg is for links the row has to scroll to", path)
+ }
+ steps := []struct {
+ key string
+ want string
+ }{}
+ for _, h := range hrefs {
+ steps = append(steps, struct{ key, want string }{"Tab", h})
+ }
+ for i := len(hrefs) - 2; i >= 0; i-- {
+ steps = append(steps, struct{ key, want string }{"Shift+Tab", hrefs[i]})
+ }
+ for _, s := range steps {
+ if s.key == "Tab" {
+ mustRun(t, ctx, chromedp.KeyEvent(kb.Tab))
+ } else {
+ mustRun(t, ctx, chromedp.KeyEvent(kb.Tab, chromedp.KeyModifiers(input.ModifierShift)))
+ }
+ var f struct {
+ Href, Style string
+ Grow float64
+ Ring, Port sideBox
+ }
+ at(t, ctx, ringJS, &f)
+ where := fmt.Sprintf("%s at %d, %s to %s", path, width, s.key, s.want)
+ if f.Href != s.want {
+ t.Errorf("%s: the focus is on %q", where, f.Href)
+ continue
+ }
+ if f.Style == "none" {
+ t.Errorf("%s: the focused link draws no outline", where)
+ }
+ if f.Grow > 0 {
+ t.Errorf("%s: the focus ring reaches %.1fpx outside the link; in the row it is drawn inside, or the row's edge cuts it wherever the link meets that edge", where, f.Grow)
+ }
+ if f.Ring.Left < f.Port.Left-0.5 || f.Ring.Right > f.Port.Right+0.5 || f.Ring.Top < f.Port.Top-0.5 || f.Ring.Bottom > f.Port.Bottom+0.5 {
+ t.Errorf("%s: the focus ring spans %.1f to %.1f across and %.1f to %.1f down, outside the row's visible %.1f to %.1f and %.1f to %.1f; the row cuts it off",
+ where, f.Ring.Left, f.Ring.Right, f.Ring.Top, f.Ring.Bottom, f.Port.Left, f.Port.Right, f.Port.Top, f.Port.Bottom)
+ }
+ }
+ }
+}
+
+// flexRows are a side in a bounded flex row: given its width by
+// flex: 1 1 0 beside a 200px column, in a wide row and a narrow one;
+// and left to size itself, as a flex item does by its content.
+const flexRows = `<div id="wide" style="display: flex; inline-size: 1000px"><div style="flex: none; inline-size: 200px">Aside</div>%[1]s</div>` +
+ `<div id="narrow" style="display: flex; inline-size: 500px"><div style="flex: none; inline-size: 200px">Aside</div>%[1]s</div>` +
+ `<div id="unsized" style="display: flex; inline-size: 1000px">%[2]s</div>`
+
+// TestASideInAFlexRowIsGivenItsWidth: the side answers to its own
+// width, so it cannot take that width from its content; a flex item
+// sized by its content is zero wide. Given flex: 1 1 0 in a bounded
+// row it fills what the row leaves, a column of links in a wide row
+// and a row of links in a narrow one. Left to size itself it
+// collapses, which is why a page must give it its width; if a browser
+// ever sizes it some other way, this says so.
+func TestASideInAFlexRowIsGivenItsWidth(t *testing.T) {
+ _, doc := sideNavExample(t)
+ side, err := example.Element(doc, "rst-side")
+ if err != nil {
+ t.Fatal(err)
+ }
+ given := strings.Replace(side, "<div rst-side>", `<div rst-side style="flex: 1 1 0">`, 1)
+ if given == side {
+ t.Fatal("the example's side is not written <div rst-side>")
+ }
+ page := strings.Replace(doc, side, fmt.Sprintf(flexRows, given, side), 1)
+ rig := sizingRig(t, false, map[string]string{"/": page})
+ ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
+ defer cancel()
+ mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitReady("#unsized", chromedp.ByQuery))
+ var got map[string]struct {
+ Width float64
+ Tops int
+ Flow string
+ Content float64
+ }
+ at(t, ctx, `(() => { const out = {};
+ for (const id of ["wide", "narrow", "unsized"]) {
+ const side = document.querySelector("#" + id + " [rst-side]"), nav = side.querySelector("[rst-side-nav]");
+ out[id] = {Width: side.getBoundingClientRect().width, Tops: new Set([...nav.querySelectorAll("a")].map(a => Math.round(a.getBoundingClientRect().top))).size,
+ Flow: getComputedStyle(nav).gridAutoFlow, Content: side.querySelector(":scope > :not([rst-side-nav])").getBoundingClientRect().width};
+ }
+ return JSON.stringify(out); })()`, &got)
+ if w := got["wide"]; !sameEdge(w.Width, 800) || w.Tops != 4 || w.Content <= 0 {
+ t.Errorf("in a 1000px row beside a 200px column, flex: 1 1 0: the side is %.1fpx wide, its links on %d lines, its content %.1fpx; want 800px, a column of four, and room for the content", w.Width, w.Tops, w.Content)
+ }
+ if n := got["narrow"]; !sameEdge(n.Width, 300) || n.Tops != 1 || n.Flow != "column" {
+ t.Errorf("in a 500px row beside a 200px column, flex: 1 1 0: the side is %.1fpx wide, its links on %d lines, flowing %s; want 300px and one row", n.Width, n.Tops, n.Flow)
+ }
+ if u := got["unsized"]; u.Width > 1 {
+ t.Errorf("left to size itself in a flex row, the side is %.1fpx wide; it was zero wide, and pages are told to give it its width: change that advice if this is now true", u.Width)
+ }
+}
diff --git a/css/tokens.css b/css/tokens.css
index 72c5c01..f63f31f 100644
--- a/css/tokens.css
+++ b/css/tokens.css
@@ -2215,10 +2215,19 @@ small.rst-field__hint, small[rst-field-hint] { display: block; font-size: var(--
the row scrolls inside the nav when it is wider than the panel, so
the page never scrolls sideways. The hairline moves under the nav.
34rem is the width at which the page's own rows give up their
- columns. */
+ columns.
+ A link reached with Tab must be wholly in view. Chromium does not
+ scroll a focused element that is already partly in view, so a link
+ half under the row's edge stayed there, cut off. scroll-padding sets
+ the part of the row it counts as in view: at 49% on each side that
+ is a sliver at the centre, so a focused link that does not cross the
+ centre is scrolled to it. The ring is drawn inside the link, as the
+ index rows draw theirs, because the row clips anything outside it
+ where a link meets the row's edge. */
@container (max-width: 34rem) {
.rst-side__nav, [rst-side-nav], .rst-side > :not(.rst-side__nav), [rst-side] > :not([rst-side-nav]) { grid-column: 1 / -1; }
- .rst-side__nav, [rst-side-nav] { border-block-end: 1px solid var(--rst-line); border-inline-end: 0; grid-auto-columns: max-content; grid-auto-flow: column; overflow-x: auto; }
+ .rst-side__nav, [rst-side-nav] { border-block-end: 1px solid var(--rst-line); border-inline-end: 0; grid-auto-columns: max-content; grid-auto-flow: column; overflow-x: auto; scroll-padding-inline: 49%; }
+ .rst-side__nav a:focus-visible, [rst-side-nav] a:focus-visible { outline-offset: -2px; }
}
/* form layout — the interview rhythm and the save bar. */