Hiding Complex Detail Sections on Phones Without Breaking Deep Links

Hiding Complex Detail Sections on Phones Without Breaking Deep Links

A small fix from the Staffpoint project, and the three edge cases that made it bigger than it looked

Staffpoint is a staffing and scheduling platform built on ASP.NET WebForms. A recent fix on the Personnel Details page dealt with a problem every responsive retrofit of an old app eventually hits: some screens simply do not work on a phone yet, and hiding a tab is not enough to keep users out of them. This post covers what the issue was, what the fix changed, and the edge cases that turned a one-line CSS change into a small routing policy.

The issue

The Personnel Details page is a single-page shell. It loads a person's record and shows a row of tabs: Details, Qualifications, Availability, Accreditations, Notes, Rates, and a few more. Each tab loads its content over AJAX into the shell, and the current tab is encoded in the URL hash, so #availability-1234 means "Availability for person 1234." That hash is what makes tabs bookmarkable and makes the Back button work.

Two of those tabs are heavy. Availability is a multi-week calendar grid with a lot of interactive cells. Accreditations is a table with create and edit forms attached. Both were built for the desktop and neither has a phone layout yet. On a phone they rendered as unusable, horizontally scrolling walls of content. The product decision was to make them unavailable on phones until a proper mobile design exists, rather than ship something broken.

The obvious fix is a media query that hides the two tabs below a breakpoint. That is where the team started, and that is where it stopped being simple. Three ways remained to land on a hidden section:

  1. Saved links. Anyone who had bookmarked #availability-1234, or received it in a message, would open the URL on a phone and the shell would happily load the Availability content into a page with no visible tab for it.
  2. Resizing. Open Accreditations on a desktop, then shrink the window or rotate a tablet, and you are now sitting inside a section that is supposed to be gone.
  3. Slow responses. Even if you redirect on resize, an AJAX request for the Availability content might already be in flight. When it comes back a second later, it overwrites the Details page you just redirected to.

The fix had to handle all three, and it had to do so without touching server permissions. This is a presentation decision about viewports, not an authorization rule.

The fix

1. A shared visibility utility

The team chose phones only as the target, defined as viewports below 768px, so tablets in portrait keep the full desktop tab set. A single utility class was added to the shared Staffpoint UI stylesheet:

/* Viewport presentation only; server permissions remain authoritative. */
@media (max-width: 767.98px) {
    .sp-u-hide-below-768 {
        display: none !important;
    }
}

That class went on the Availability and Accreditations tab links and on their content panels. Because the page's responsive tab dropdown derives its options from the original tab links, the hidden tabs also disappear from the dropdown for free. No separate mobile navigation list had to be maintained.

2. A route guard for saved links

The hash router got a guard that runs before any section is loaded. It matches the hashes for Availability, Accreditations, and the accreditation create and edit forms, and checks the viewport with the same breakpoint as the CSS:

function IsStaffPhoneViewport() {
    return window.matchMedia('(max-width: 767.98px)').matches;
}

function RedirectStaffPhoneSection(hash) {
    var section = /^#(?:availability|accreditations|newaccreditation|editaccreditation)-(\d+)(?:-\d+)?$/.exec(hash);
    if (!section || !IsStaffPhoneViewport()) return false;
    // Replace the unavailable entry so Back does not bounce through it.
    window.history.replaceState(window.history.state, '', '#' + section[1]);
    LoadStaff(section[1]);
    return true;
}

Two details matter here. The redirect keeps the same person ID, so a saved Availability link for person 1234 lands on Details for person 1234, not on the search list. And it uses replaceState rather than a new hash, so the unavailable entry is removed from history. Pressing Back does not bounce the user through a section they cannot see.

3. A resize listener

The same guard is wired to the window resize event. If a restricted section is open and the viewport crosses below 768px, the user is returned to Details. Widening again does not reopen the section. The tabs simply reappear and Details stays selected, which is the least surprising behavior.

4. Discarding stale responses

This is the part that is easy to forget. Every AJAX callback that renders a restricted section now captures the hash at request time and checks, when the response arrives, that the viewport is still desktop-sized and the hash has not changed:

function CanShowStaffDesktopSection(requestHash) {
    return !IsStaffPhoneViewport() && location.hash === requestHash;
}

function StaffAccreditations(id) {
    var requestHash = location.hash;
    ShowLoader();
    $.post(url, function (data) {
        if (!CanShowStaffDesktopSection(requestHash)) return;
        // render as before
    });
}

If the user resized or navigated while the request was in flight, the response is thrown away. Without this, the redirect to Details would work for a moment and then be overwritten by the late Availability payload. The check was applied to the Accreditations table, the new and edit accreditation forms, and both stages of the Availability load, since Availability first fetches the shell and then the calendar content.

How it was validated

Staffpoint has no browser test suite, so the team wrote a set of non-browser tests with jsdom covering the route logic: direct links to each restricted hash, the exact 768px boundary, resize in both directions, history preservation, unrelated routes passing through untouched, and five variations of a response arriving after navigation or resize. A second set of assertions loaded the real shared dropdown and the utility CSS to confirm phone versus tablet eligibility. Visual acceptance on actual devices stayed with the product owner.

What the fix deliberately did not do

It did not change any server-side permission or handler. A phone user who crafts a request to the Availability endpoint gets exactly what they got before, because the endpoint's own authorization is what protects it. It also did not reclassify other document entry points on the page, and it did not attempt a mobile layout for the two sections. The whole change is designed to be removable: when a mobile design for Availability and Accreditations ships, delete the utility class from four elements and the route guard, and the sections come back.

Takeaways

Hiding a tab is a CSS problem. Keeping users out of the section behind it is a routing problem, and in a hash-routed single-page shell that means three things: guard direct navigation, guard resize, and guard late responses. Use one breakpoint constant in both CSS and JavaScript so the two never disagree. Replace history entries instead of pushing new ones so Back stays sane. And make the whole thing a presentation policy, not a permission, so the server stays the single source of truth for who can see what.

Responsive DesignJavaScriptASP.NETMobile WebLegacy CodeCase Study

Comments

Popular posts from this blog

Featured Projects: Automation and AI Systems I Built and Run

AWS API gateway, S3

Business case: Monitor mailbox and auto-save the attachment to a SharePoint