Keep the dock's tabs in sight around the date strip, on phones and narrow desktop windows
67a21b4 let the dock's tabs give way to the date and range strips and
scroll, and measured phones only after Go. Reviewers found four things it
did not measure:
- At the present, the state the map opens in, the Range strip, now 121 px
and unshrinkable, took the tabs' width on a portrait phone: Display showed
0 of 79 px at 360, 390 and 412 wide, and Bookmarks 0, 27 and 49 of 95.
Before, the strip sat off screen at x 406-502. It is now hidden below sm.
- After Go on a phone, focus went back to the Display tab while the list
was still wide; the strip then narrowed it and its scroll stayed, so the
focused tab was 0 of 79 px in sight (a focus ring off screen), and on
Saturn's page the Clock tab 28 of 63. The focused tab, else the selected
one, is now scrolled into view after each render in which the strip comes
or goes (afterRenderEffect on whether a date is shown).
- On desktop windows 600 to 770 px wide the tab list drew the browser's
classic scrollbar, light and 15 px tall, in the dark dock, and at 640 the
open Display panel's tab was scrolled out of sight. The list now asks for
a thin dark one (scheme-dark, scrollbar-width: thin); the tab is brought
back as above.
- The range strip's shrink-0 had no test: without it '417 AU' wraps and the
row grows from 38 to 58 px.
Measured on :4301 in the Sun's system, keyboard only (Tab to Display,
Enter, Tab to the date, 2020-12-21T18:00, Enter), visible px of each tab:
- 360x640 phone, at the present: Bookmarks 95/95, Display 24/79 (was 0),
range hidden. After Go: the focused Display 78/79 (was 0), the date on
screen, no page scroll. 390x844: 54/79 then 78/79. 412x915: 76/79 then
78/79.
- Saturn's page at 360x640, after Go: the focused Clock tab 63/63.
- 600x800 desktop: every tab in full before and after Go, no scrollbar.
- 640x900, 700x900, 768x1024 desktop, after Go: the open Display tab 87/87,
a dark scrollbar 10 px tall (row 43 px, was 48 with a light one).
- 1400x900: unchanged, row 38 px.
With no strip at all, a phone's row is now 33 px tall at the present and
38 once a date is set; the off-screen range strip used to hold it at 38.
Tests: 'keeps the date strip on screen on a phone' now also sets a range
and expects it shrink-0 and max-sm:hidden, and the list scheme-dark and a
thin scrollbar; 'brings the tab that matters back into view once the date
strip has narrowed the tabs' checks the focused tab on a phone and the
selected tab on a wide screen. Guarded mutants, each failing its named test
alone: the range's shrink-0 dropped, the range shown on a phone, the
default scrollbar, no tab scrolled into view, the selected tab only, and
the effect not keyed on the strip. Unit suite 870 passed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -485,10 +485,51 @@ describe('HudDockComponent', () => {
|
||||
// At 360 px the system view's five tabs take 397 px, and pushed the strip past the right
|
||||
// edge, where nothing scrolls: after Go on a phone the date was nowhere on screen.
|
||||
fixture.componentRef.setInput('date', '2020-12-21');
|
||||
fixture.componentRef.setInput('range', '417 AU');
|
||||
fixture.detectChanges();
|
||||
const tabs = host().querySelector('[role="tablist"]')!.classList;
|
||||
expect(tabs.contains('min-w-0') && tabs.contains('overflow-x-auto')).toBe(true);
|
||||
// Its own scrollbar, where the browser draws one, thin and dark: a desktop's default was a
|
||||
// light bar 15 px tall across the dock.
|
||||
expect(tabs.contains('scheme-dark') && tabs.contains('[scrollbar-width:thin]')).toBe(true);
|
||||
expect(host().querySelector('[data-testid="hud-date"]')!.classList.contains('shrink-0')).toBe(true);
|
||||
// The range keeps its width where it is shown, or '417 AU' wraps and the row grows 20 px; on a
|
||||
// phone it is not shown, where at the present it took the Display tab out of sight.
|
||||
const range = [...host().querySelectorAll('p')].find((p) => p.textContent?.includes('Range'))!.classList;
|
||||
expect(range.contains('shrink-0') && range.contains('max-sm:hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('brings the tab that matters back into view once the date strip has narrowed the tabs', () => {
|
||||
// jsdom lays nothing out; what is checked is which tab is asked to be in view, and when.
|
||||
const scrolled: string[] = [];
|
||||
HTMLElement.prototype.scrollIntoView = function (this: HTMLElement) {
|
||||
scrolled.push(this.id);
|
||||
};
|
||||
try {
|
||||
// On a phone: the tab focus went back to, which after Go sat wholly out of sight.
|
||||
viewport.wide = false;
|
||||
host().querySelector<HTMLInputElement>('#clock-date')!.value = '2020-12-21T18:00';
|
||||
button('Go').click();
|
||||
fixture.detectChanges();
|
||||
scrolled.length = 0;
|
||||
fixture.componentRef.setInput('date', '2020-12-21');
|
||||
fixture.detectChanges();
|
||||
expect(scrolled).toEqual(['dock-tab-display']);
|
||||
// On a wider window, where the panel stays open: its tab, focus being in the panel.
|
||||
viewport.wide = true;
|
||||
fixture.componentRef.setInput('date', '');
|
||||
fixture.detectChanges();
|
||||
tab('Display').click();
|
||||
fixture.detectChanges();
|
||||
host().querySelector<HTMLInputElement>('#clock-date')!.focus();
|
||||
scrolled.length = 0;
|
||||
fixture.componentRef.setInput('date', '2020-12-21');
|
||||
fixture.detectChanges();
|
||||
expect(scrolled).toEqual(['dock-tab-display']);
|
||||
} finally {
|
||||
viewport.wide = true;
|
||||
delete (HTMLElement.prototype as Partial<HTMLElement>).scrollIntoView;
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the panel open on a wide screen, where it covers little of the scene', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
afterRenderEffect,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
@@ -416,8 +417,14 @@ function isWideViewport(): boolean {
|
||||
|
||||
<div class="hud-brackets hud-surface pointer-events-auto flex w-full items-stretch">
|
||||
<!-- The tabs give way to the date and the range, and scroll: the five of the system view
|
||||
take 397 px, and on a portrait phone they pushed the date off the right edge. -->
|
||||
<div role="tablist" aria-label="Dock" class="flex min-w-0 items-stretch divide-x divide-border/40 overflow-x-auto">
|
||||
take 397 px, and on a portrait phone they pushed the date off the right edge. Where
|
||||
the browser draws a scrollbar of its own, it is a thin dark one: on a desktop window
|
||||
under 770 px wide its default was a light bar 15 px tall across the dark dock. -->
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Dock"
|
||||
class="flex min-w-0 scheme-dark items-stretch divide-x divide-border/40 overflow-x-auto [scrollbar-width:thin]"
|
||||
>
|
||||
@for (tab of tabs(); track tab) {
|
||||
<button
|
||||
type="button"
|
||||
@@ -448,9 +455,11 @@ function isWideViewport(): boolean {
|
||||
<span class="text-sm text-accent tabular-nums">{{ date() }}</span>
|
||||
</p>
|
||||
}
|
||||
<!-- Not on a phone, where it never showed before the tabs gave way to it: kept, its 121 px
|
||||
took the Display tab out of sight at the present, the state the map opens in. -->
|
||||
@if (range()) {
|
||||
<p
|
||||
class="flex shrink-0 items-baseline gap-2 border-l border-border/40 px-3 py-2 sm:px-4"
|
||||
class="flex shrink-0 items-baseline gap-2 border-l border-border/40 px-3 py-2 max-sm:hidden sm:px-4"
|
||||
[class.ml-auto]="!date()"
|
||||
>
|
||||
<span class="type-label text-muted">Range</span>
|
||||
@@ -527,6 +536,23 @@ export class HudDockComponent implements OnInit {
|
||||
private readonly search = viewChild(SearchComponent);
|
||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
|
||||
private readonly dateShown = computed(() => this.date() !== '');
|
||||
/**
|
||||
* The tab list gives way to the date strip but keeps its scroll, so once the strip is drawn the
|
||||
* tab that matters can be wholly out of sight: after Go on a phone, the tab focus went back to
|
||||
* (0 of its 79 px at 360, 390 and 412 wide), and on a desktop window 640 to 770 px wide, the tab
|
||||
* of the panel left open. Brought back into view whenever the strip comes or goes: the focused
|
||||
* tab, else the selected one.
|
||||
*/
|
||||
private readonly keepTabInView = afterRenderEffect(() => {
|
||||
this.dateShown();
|
||||
const list = this.host.nativeElement.querySelector('[role="tablist"]');
|
||||
const focused = document.activeElement;
|
||||
const tab = focused && list?.contains(focused) ? focused : list?.querySelector('[aria-selected="true"]');
|
||||
// Optional: jsdom, which the unit tests run in, lays nothing out and has no scrollIntoView.
|
||||
tab?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activeTab.set(isWideViewport() ? this.defaultTab() : null);
|
||||
this.fillDateField();
|
||||
|
||||
Reference in New Issue
Block a user