Skip to content

Mobile Friendly Domains

Ed Mozley edited this page Oct 2, 2026 · 2 revisions

Mobile‑Friendly: Domains

Scope: all seven pages β€” the register, the table view, the dashboard, registrar accounts, a domain's page (and its Edit dialog), settings, and the guide. Layers: mobile.css LAYERS 41a–41h, and mobile.js LAYER 41 (the register's filter sheet) plus two FEEDS entries. Parent: Mobile‑Friendly Β· Techniques: Β§3, Β§4, Β§10, Β§11, Β§21, Β§23, Β§28, Β§31


Where it started

Domains arrived after the rollout had finished, so it was built with mobile.css linked on every page, the link correctly after each page's own CSS (Β§9), markers on five of seven pages, and a @media (max-width: 900px) block of its own that stacks the register's sidebar above the list. Nothing spilled sideways on any page. That is a good starting position, and also why the measurement run alone would have called it done.

Measured at 360Γ—740 through an explicit‑width iframe (Β§8, Β§30), then looked at:

Page Found Answer
Register Search, ten views and six filters stacked above the list β€” a screen and a half before the first domain. Six stats one per row. A ten‑column table cut off after Status. Sidebar β†’ a Β§4 sheet behind a Filters bar; stats two‑up; the table β†’ a card feed
Dashboard Seven stats one per row Two‑up
A domain Six tabs wrapping into three ragged rows; "All checks" 14px past its card One swipeable tab row; card headings wrap
Accounts Seven columns clipped after two A card feed
Table view β€” Already answered by the shared .dt-* layer (LAYER 15)
Settings β€” Already answered by the shared settings layer (tabs swipe, config tables scroll sideways)
Guide β€” Already answered by the shared guide layer
Every page Every field 13px 16px module‑wide (Β§3)

The scrolling shell: an inline style becomes a class

Three pages (dashboard, accounts, a domain) scroll inside a wrapper that was written inline:

<div style="height:calc(100vh - 62px);overflow-y:auto;background:var(--app-bg,#f5f5f5)">
    <div style="padding:20px 26px 40px">

Two problems on a phone: the header is not 62px, and 100vh runs under Safari's address bar, hiding the bottom of the page. And an inline style can only be beaten with !important.

Β§9's third option β€” give the markup a class and change nothing else β€” taken one step further: the inline styles moved into domains.css as .dom-shell / .dom-shell-pad with the same values, so the phone layer has a normal selector:

body[data-mobile-module="domains"] .dom-shell {
    flex: 1 1 auto; height: auto; min-height: 0;   /* Β§28: min-height or nothing scrolls */
    overflow-y: auto; overflow-x: hidden;
}

Moving a style out of the markup is a desktop change, so it is proven rather than argued: at 1100px the shell computes height 738 (= innerHeight βˆ’ 62), overflow-y: auto, padding: 20px 26px 40px, and the desktop control below returns identical.

The register

The sidebar as a sheet

mobile.js moves the real .dom-sidebar into a full‑screen sheet opened from a bottom Filters bar, and moves it home when the viewport leaves mobile β€” so every listener domains-register.js attached keeps working, and the desktop never builds the sheet or the bar. The bar shows the current view beside the word ("Filter Β· Needs attention"), harvested from the view's own button, so the list's state is visible without opening anything and no string was invented.

The sheet closes on a view (an arrival β€” what you chose is behind it) and stays open on the search box and the drop‑downs, because you often set several. That distinction is the System Wiki round's lesson.

πŸ”΄ The listener that never saw its own tap

The first run: tap a view, the list changed β€” and the sheet stayed open on top of it.

sheetBody.addEventListener('click', function (e) {
    var btn = e.target.closest('#domViews button');   // null!
    if (btn) setOpen(false);
});

The page's own click handler re‑renders #domViews to move the highlight. Bubbling runs after it, so by then the tapped button has been replaced and is no longer inside #domViews β€” closest() walks up a detached node and finds nothing. The fix is one argument: listen in the capture phase, which runs before the page's handler.

sheetBody.addEventListener('click', handler, true);

πŸ”‘ A delegated listener that asks "where was this tapped?" must ask before the page redraws the answer. This is Β§31's test again β€” change the choice and assert the screen changed β€” and only driving it found it: every measurement of the open sheet passed.

The table as a card feed

Ten known columns, read one domain at a time: a feed (Β§11). Status, name and grade speak for themselves; the expiry, renewal, registrar, certificate and owner get their headings back (Β§21), harvested from the hidden <thead>:

{ table: 'body[data-mobile-page="domains-register"] #domTable', columns: [3, 4, 5, 7, 8, 9] }

The protection icons (lock, registry lock, DNSSEC) are SVG with no text, so the harvester rightly declines to label them; they lead the facts line as a set of badges instead.

⭐ Three passes to one card shape

The feed took four screenshots to get right, and every one of them passed every measurement.

  1. Every fact on its own line β†’ seven‑line cards that read like a spreadsheet. The facts now flow as one wrapping line, and the checkbox is absolutely positioned in the corner with the card indented past it, so no cell needs its own offset.
  2. Β§23 met in the data β€” with flex: 1 1 60% on the name, a short name let the status climb onto the first line and a long one did not. Name and grade now fill the line exactly, so the status always starts the second:
    #domTable tbody td:nth-child(2) { flex: 1 1 calc(100% - 44px); }   /* + 28px grade + 12px gap */
  3. A long status ("Letting lapse") wrapped the expiry, and the icons rode up beside it on the right. A forced line break fixed it β€” a zero‑height, full‑width flex item placed by order:
    #domTable tbody tr::after { content: ""; order: 5; flex: 0 0 100%; height: 0; }
    #domTable tbody td:nth-child(8) { order: 6; }          /* the icons start the facts line */

⭐ A flex row has no "new line here" β€” but a full‑width item of zero height is one. Put it at the order where the line must break and everything after it starts at the left, whatever the content before it did. It is the deterministic answer when Β§23's middle basis is not enough because two lines depend on the data.

A domain's page

The six tabs (#domTabs, the shared .tabs/.tab classes) become one swipeable row β€” the same idiom as the settings tabs and .detail-tabs. Card headings that carry a link ("All checks", "See all") now wrap instead of poking past the card. The Edit dialog is a .modal-content, so LAYER 3 already makes it a full‑screen sheet; measured anyway: 28 fields all inside, the three date fields already reset by LAYER 3 and still fitting when forced to the 210px an iPhone needs (Β§20), the customer picker's results inside the sheet, Save on screen.

The actions in a sticky footer (41i, Ed)

Refresh, Check now, Edit and Delete sat in a row under the hero and scrolled away as soon as you read down the page, and "← All domains" was a small grey text link. On a phone all five now live in a footer pinned to the bottom - Back first as a chevron square, then the four actions as icons, Edit in the module's green and Delete in red, set apart on the right.

It is the Checklists editor's footer, done the same way: the real link and buttons move (each is bound by id in domains-view.js, so a copy would do nothing), each one's own label becomes its aria-label (Refresh and Check keep the icon they already carry), the footer's height is measured into --dom-vbar-h and reserved, and above 768px every node goes back to its exact place. The row they left, and the link's wrapper, are hidden so no empty strip remains.

⚠️ busy() rewrites a button while it works - it saves the button's contents, shows Working…, then restores what it saved. Because the save happens at click time, what comes back is the icon; the four action buttons share the width (62px) rather than being 52px squares, so Working… fits.

⚠️ The harness said the delete confirmation did not cover the bar. It does: confirm.js adds its active class inside a requestAnimationFrame, and animation frames do not run in a headless iframe, so the overlay sat at visibility: hidden and the bar's Edit button was under the finger. Adding the class the frame would add showed the overlay (z-index 99998) on top. Disabling transitions was not enough, because the class is never added at all - a third kind of §30 harness lie, next to transitions and behavior: 'smooth'.

Verification

At 360Γ—740, on every page: nothing off either edge (left < 0 included); the page's scroller found, scrolled to the end, and what is under the finger changed (Β§13, Β§28); every field 16px (the one exception is a colour picker, excluded by design); no script errors. On the register: the sheet opens, holds the real sidebar, closes on a view, the list changes, the bar's label follows, and a drop‑down keeps it open; 45 labelled cells.

⚠️ One red from the first run was the probe's own (§30's addendum): it took a hidden dialog's body for the page's scroller on two pages. Excluding .modal found the real one, .dom-shell, scrolling to the end.

Desktop control at 1100px on all seven pages β€” fingerprint every element's box, disable mobile.css in place, fingerprint again: identical, 5,695 boxes. Plus the Β§30 blind spot: the Filters bar and sheet are never built at desktop width, the sidebar is in its home, and no data-mobile-label is stamped.

Still owed

  • The dashboard's grade chart clips its last legend entry ("Not checked yet") at 360px. The legend is drawn by Chart.js on a canvas, so CSS cannot reach it; it needs the chart's own legend position to change at phone width, which is a domains.js change, not a layer.
  • A device pass. Everything here is headless; iOS gesture handling and the real address bar are not.

See also: Domains Β· Mobile‑Friendly Β· Techniques Β· Mobile: Checklists (the same filter‑sheet pattern)

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally