Skip to content

Mobile Friendly Self Service

Ed Mozley edited this page Sep 6, 2026 · 1 revision

Mobile‑Friendly: Self‑Service Portal

The portal is the public‑facing half of FreeITSM and the one surface most requesters only ever see on a phone. Part one shipped in #1207; part two in #1491–#1494, self-service.css v13.

Read Mobile‑Friendly first for the strategy and the one hard rule, and Techniques & Tricks for the catalogue this draws on.


πŸ”‘ The portal keeps its OWN layer β€” and this is the decision that shapes everything else

It does not opt into mobile.css. Ed's call, and the reasoning still holds: the portal shares none of the analyst shell β€” no .header-nav, no waffle, no .main-container β€” so pulling in twelve and a half thousand lines of rules written against analyst markup would drag Β§15's hazard across a public surface, and .summary-cards, .search-box, .form-input and .modal-content already exist on both sides.

So: techniques from the wiki, stylesheet local. The mobile layer is a @media (max-width: 768px) block appended to assets/css/self-service.css, after the pre‑existing 900/640/500 blocks so it wins on document order.

⚠️ That has a running cost, and part two is where the bill arrived. Twice in this round the answer already existed in mobile.css and had to be written again here β€” the guide's scroller (LAYER 16h) and its contents‑strip fix (#1464). It is still the right call; it is simply not a free one, and knowing which is which is the point of writing it down.


Part one (#1207) β€” the shell

  • πŸ”΄ Ed overruled the nav, and he was right. It was first built as a scrolling chip row; he asked why it was not a ☰ drawer like every other module. The codebase settles it β€” mobile.js's injectViewsHamburger() turns .header-nav into a drawer on every module, and .portal-nav is structurally the same thing.

    πŸ”‘ The chip rows in this rollout are for FILTERS, not DESTINATIONS. .filter-chips, .filter-tabs and .detail-tabs are all filters. Check which pattern a precedent is before copying it.

  • πŸ”΄ The drawer was on screen, correctly coloured, 44px per link β€” and completely untappable. .portal-header is position: sticky; z-index: 100, which creates a stacking context, so the nav's z-index: 3000 resolved inside it and the whole drawer painted at level 100 β€” beneath its own overlay at 2999. Every geometry assertion passed; elementFromPoint on a link returned .ss-nav-overlay. Fixed with body.ss-nav-open .portal-header { z-index: 3000 }, only while open.

    ⚠️ A fixed‑position drawer inside a sticky, z‑indexed header is trapped in its parent's stacking context. Assert elementFromPoint, never the rect.

  • ⚠️ Two pre‑existing rules had never applied to anything β€” both of which look like the problem is handled:

    rule why it did nothing
    @640 .portal-nav a { padding: 6px 10px } padding cannot beat a min-width: 104px floor β€” the floor was the constraint
    @500 .ticket-table .col-optional { display: none } col-optional appears nowhere in the markup
  • ⭐ The brand wordmark stays on mobile. The 640px block hid .portal-brand span outright to buy nav room; with the nav in a drawer that room is not needed, and hiding it deletes the customer's identity on the device most requesters use. It truncates instead. Ed asked the question that found this.


Part two (#1491–#1494)

πŸ”΄πŸ”΄ The guide could not be scrolled at all, and three files were each individually right

help.php   7118px of content
           2 UNREACHABLE:  HTML 7118/740  |  BODY.portal-app 7118/740

Ten of eleven sections unreachable, and no single rule to blame:

file rule and it is correct
self-service.css body.portal-app { height: 100vh; overflow: hidden } the app shell β€” on every other portal page a pane scrolls inside it
help.css @900px .help-container { height: auto }, .help-main { overflow-y: visible } hands the scroll outwards, which is right for a document that can take it
β€” nothing gives the document permission

This is Β§28, and the same three‑file interaction the Network Mapper round found on the analyst guides.

⚠️ And it is THREE elements, not two β€” the fix needed the whole chain

The first attempt made <body> a flex column and gave .help-container flex: 1 1 auto. Nothing changed, because .help-container is not a child of <body>: the portal header include wraps every page in a .portal-layout, which stayed display: block and grew to its content.

DIV.help-container   flex: 1 1 auto   height 7069px   <- resolved
DIV.portal-layout    display: block   height 7069px   <- against THIS
BODY.portal-help     display: flex    height  740px

πŸ”‘ A flex: 1 is measured against its own parent, so every element between the viewport‑height box and the scroller has to pass the height down. Measuring the container alone said flex: 1 1 auto and looked correct. What found it was dumping the entire ancestor chain β€” display, height, min‑height, flex and overflow for each β€” which is now the first thing to print when a scroller will not take.

Scoped to a new body.portal-help class, because making <body> a flex column is not something any other portal page wants.

πŸ”΄ The contents strip was tappable and inert

The page's own handler scrolls #helpMain β€” right on a desktop, where .help-main is the scroller; below 900px that element is overflow-y: visible and scrollTo on it throws nothing and does nothing. The highlight never moved off "1".

The portal cannot use mobile.js's #1464 fix, so the page's handler now resolves the scroller on every use rather than caching it, and listens on both candidates. Verified at both widths by intercepting the call rather than reading scrollTop β€” smooth scrolling never advances under --virtual-time-budget:

@360  ->  help-container <- {top: 557,  behavior: smooth}
@1100 ->  help-main      <- {top: 365,  behavior: smooth}

πŸ”΄ A 17px tap target, and the third rule in this module that had never applied

a.ticket-link  90x17   display: inline   min-height: 24px

min-height is inert on an inline box, and an <a> wrapping text is inline β€” so round one's rule computed to 24px and changed nothing. It matters more than the number suggests: the <tr> has no click handler, so those two anchors are the only way to open a ticket from the page a requester lands on.

The fix is the display type, not a bigger number.

⚠️ And not 44px on both of them, which is what the first version did β€” and only a screenshot showed why. There are two anchors per card, so forcing a 44px block on each added ~50px of empty space to every card and left the reference floating above a gap. Every measurement was happier and the card looked broken. The height goes where the target is: a full‑width 44px row for the subject, a 32px inline box for the reference.

πŸ”΄ Β§15 again β€” two tables, one class, different columns

Your requests and Recent tickets are both .ticket-table, but with three columns and five. The phone card rules key on column index, so the requests table was having its status cell styled as a ticket's subject: the approval pill given its own line with 8px of margin under it, the reference stranded, the date pushed alongside.

The renderer now stamps req-table and the index rules are scoped :not(.req-table) β€” the same answer as the two log tables in Reporting, and exactly what LAYER 15b's own comment warned about when it said nothing here may depend on a column INDEX.

⚠️ Its own shape is display: block per cell, not inline-flex on the status cell, which is what the first attempt used: the base rule leaves a td display: inline, so an inline‑flex status cell and an inline date cell shared one line box, and once the date joined it the flex box was handed a narrower width and wrapped internally β€” pill on one line, reference on the next, date beside the reference. Three items, three places, none of them chosen.


Harness notes specific to the portal

  • The session key is ss_user_id, not analyst_id. Get the s: byte lengths exactly right or PHP discards the whole session and every page 302s, which looks like an auth failure.
  • πŸ”΄ login.php and register.php redirect when a session exists, so a cookie‑carrying harness measures index.php and labels it "login" β€” round one had two of eight results be fiction that way. Run pre‑auth pages with no cookie, and print the resolved location.pathname on every result so the page can prove which page it is.
  • register.php still 302s on this install because self‑registration is off; it cannot be measured without enabling it first.
  • ticket.php redirects to tickets.php β€” the detail lives in the list page, so there is no separate detail page.
  • Kill transitions on every run, not only for screenshots. Headless Chrome never advances them, so a drawer reads as off‑screen and invisible after being opened.
  • ⚠️ The shrink‑floor detector needs two exclusions, one per axis. Round one recorded the horizontal one: text-overflow: ellipsis + nowrap reports as a floor and is the feature (.tk-item-preview showed "+602px" and was perfect). Part two added the vertical twin: -webkit-line-clamp: 3 with overflow: hidden is a deliberate three‑line preview whose content is supposed to be taller than its box β€” .article-card-preview reported 126/54 six times on the dashboard and was working exactly as designed.

Verification

check result
width containment, 8 pages @360Γ—740 docScrollW = 360 on every one
reachability (Β§28) a real scroller or a genuine fit on all eight; the guide had neither
320Γ—640 green on all four pages re‑run
anti‑zoom sweep every visible field on every page β‰₯ 16px
the ☰ drawer 266Γ—740, z: 3000, header promoted only while open, all four destinations 242Γ—44 and elementFromPoint reaches each
the reading pane list hidden, 44px back bar, thread scrolls, composer fields β‰₯16px
the contents strip jumps to the right offset at both widths, active chip follows
the guide on desktop @1100 body back to display: block, .help-main the scroller again, handler resolving to it
every CSS change inside the @media (max-width: 768px) block β€” verified by line number against the block's own bounds
req-table / portal-help referenced only inside that block; no styling at any other width

⬜ Owed: self-service.nav.menu β€” the ☰ button's accessible name β€” exists in 9 of 25 locales. t() falls back to English, so the other sixteen announce "Menu" in English rather than the key, which is graceful rather than broken. It belongs in the i18n fan‑out, not in a guess at sixteen translations.

⬜ Not measured: register.php, which redirects while self‑registration is off.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally