Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions assets/css/builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -2406,3 +2406,21 @@ os-checkbox-label.atfb-check {
min-inline-size: 0;
flex: 1 1 auto;
}

/* -------------------------------------------------------------------------- */
/* The window body never scrolls */
/* -------------------------------------------------------------------------- */

/* The shell gives a native window's body `overflow: auto`, and under some
sequences of tab switches and resizes the browser decides these windows have
scrollable overflow even though every pane in them manages its own. A wheel
tick then hides the toolbar under the title bar and opens a dead band at the
bottom. Nothing here ever wants the body scrolled: this stops the wheel, and
`pinWindowBodyScroll()` in ui.ts undoes the programmatic scrolls CSS cannot
reach. */
.os-window__body:has( > .atfb ),
.os-window__body:has( > .atfe ),
.os-window__body:has( > .atfs ),
.os-window__body:has( > .atfa ) {
overflow: hidden;
}
21 changes: 21 additions & 0 deletions assets/js/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,26 @@ var allTerrainFormsAnalytics = function(exports) {
}
console.info(`[AllTerrain Forms] ${title}${body ? `: ${body}` : ""}`);
}
function pinWindowBodyScroll(root) {
const body = root.closest(".os-window__body");
if (!body) {
return;
}
body.addEventListener(
"scroll",
() => {
if (body.scrollTop) {
body.scrollTop = 0;
}
if (body.scrollLeft) {
body.scrollLeft = 0;
}
},
{ passive: true }
);
body.scrollTop = 0;
body.scrollLeft = 0;
}
const NPS_BANDS = [
{ key: "detractors", label: "Detractors", hint: "0–6" },
{ key: "passives", label: "Passives", hint: "7–8" },
Expand Down Expand Up @@ -845,6 +865,7 @@ var allTerrainFormsAnalytics = function(exports) {
return;
}
root.dataset.atfaMounted = "1";
pinWindowBodyScroll(root);
mounted = new AnalyticsWindow(root);
void mounted.start();
}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/analytics.min.js

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions assets/js/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,26 @@ var allTerrainFormsBuilder = function(exports) {
} catch {
}
}
function pinWindowBodyScroll(root) {
const body = root.closest(".os-window__body");
if (!body) {
return;
}
body.addEventListener(
"scroll",
() => {
if (body.scrollTop) {
body.scrollTop = 0;
}
if (body.scrollLeft) {
body.scrollLeft = 0;
}
},
{ passive: true }
);
body.scrollTop = 0;
body.scrollLeft = 0;
}
function hosts() {
const found = [window];
try {
Expand Down Expand Up @@ -1757,6 +1777,19 @@ var allTerrainFormsBuilder = function(exports) {
);
}
}, 180);
const paintNow = (written) => {
const wrap = preview.querySelector(".atf-form-wrap");
if (!wrap) {
return;
}
for (const [token, value] of Object.entries(written)) {
if ("" === value) {
wrap.style.removeProperty(`--atf-${token}`);
} else {
wrap.style.setProperty(`--atf-${token}`, value);
}
}
};
const resolve = (token) => {
if (overrides[token.token] !== void 0) {
return overrides[token.token];
Expand Down Expand Up @@ -1801,6 +1834,7 @@ var allTerrainFormsBuilder = function(exports) {
active = theme.slug;
replaceOverrides({});
options.onTheme(active);
paintNow(theme.resolved);
renderThemes();
renderQuick();
renderControls();
Expand All @@ -1826,6 +1860,7 @@ var allTerrainFormsBuilder = function(exports) {
overrides[token] = value;
options.onOverride(token, value);
}
paintNow(written);
renderQuick();
renderControls();
repaint();
Expand Down Expand Up @@ -1927,6 +1962,7 @@ var allTerrainFormsBuilder = function(exports) {
overrides[token.token] = next;
}
options.onOverride(token.token, next);
paintNow({ [token.token]: next });
repaint();
};
let control2;
Expand Down Expand Up @@ -2084,6 +2120,7 @@ var allTerrainFormsBuilder = function(exports) {
overrides[name] = String(value);
options.onOverride(name, String(value));
}
paintNow({ ...overrides });
renderControls();
repaint();
} catch (error) {
Expand Down Expand Up @@ -2172,6 +2209,7 @@ var allTerrainFormsBuilder = function(exports) {
return;
}
root.dataset.atfsMounted = "1";
pinWindowBodyScroll(root);
const bar = root.querySelector("[data-atfs-bar]");
const body = root.querySelector(".atfs__body") ?? root;
try {
Expand Down Expand Up @@ -5445,6 +5483,7 @@ var allTerrainFormsBuilder = function(exports) {
}
root.dataset.atfbMounted = "1";
mountedRoot = root;
pinWindowBodyScroll(root);
void whenComponents().then(() => {
if (!root.isConnected) {
return;
Expand Down
6 changes: 3 additions & 3 deletions assets/js/builder.min.js

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions assets/js/entries.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,26 @@ var allTerrainFormsEntries = function(exports) {
timer = window.setTimeout(() => fn(...args), wait);
};
}
function pinWindowBodyScroll(root) {
const body = root.closest(".os-window__body");
if (!body) {
return;
}
body.addEventListener(
"scroll",
() => {
if (body.scrollTop) {
body.scrollTop = 0;
}
if (body.scrollLeft) {
body.scrollLeft = 0;
}
},
{ passive: true }
);
body.scrollTop = 0;
body.scrollLeft = 0;
}
const FORM_TYPE = "allterrain-forms/form";
const ENTRY_TYPE = "allterrain-forms/entry";
function relations() {
Expand Down Expand Up @@ -1333,6 +1353,7 @@ var allTerrainFormsEntries = function(exports) {
}
root.dataset.atfeMounted = "1";
mountedEntriesRoot = root;
pinWindowBodyScroll(root);
void whenComponents().then(() => {
if (!root.isConnected) {
return;
Expand Down
2 changes: 1 addition & 1 deletion assets/js/entries.min.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*/

import { api, runtime } from './api';
import { button, clear, confirmAction, el, notify, select, whenComponents } from './ui';
import { button, clear, confirmAction, el, notify, pinWindowBodyScroll, select, whenComponents } from './ui';
import type { AnalyticsReport, Breakdown, DemoStatus, FieldReport, FormSummary, NumberSummary } from './types';

/** How the NPS bands are coloured, and what they are called. */
Expand Down Expand Up @@ -790,6 +790,7 @@ export function mountAnalytics(): void {
}

root.dataset.atfaMounted = '1';
pinWindowBodyScroll( root );
mounted = new AnalyticsWindow( root );

void mounted.start();
Expand Down
2 changes: 2 additions & 0 deletions src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
readSetting,
whenComponents,
row,
pinWindowBodyScroll,
select,
textArea,
textInput,
Expand Down Expand Up @@ -4138,6 +4139,7 @@ export function mountBuilder(): void {
// pass it and mount two builders on one page.
root.dataset.atfbMounted = '1';
mountedRoot = root;
pinWindowBodyScroll( root );

void whenComponents().then( () => {
// The window may have been closed while the kit was loading.
Expand Down
2 changes: 2 additions & 0 deletions src/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
select,
textInput,
whenComponents,
pinWindowBodyScroll,
} from './ui';
import { entryIdentity, setIdentity } from './relations';
import type { Entry, FormSummary } from './types';
Expand Down Expand Up @@ -808,6 +809,7 @@ export function mountEntries(): void {
// the guard and the flag lets both through.
root.dataset.atfeMounted = '1';
mountedEntriesRoot = root;
pinWindowBodyScroll( root );

void whenComponents().then( () => {
if ( ! root.isConnected ) {
Expand Down
37 changes: 36 additions & 1 deletion src/theme-studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { api } from './api';
import { button, clear, confirmAction, debounce, el, notify, row, select, textInput } from './ui';
import { button, clear, confirmAction, debounce, el, notify, pinWindowBodyScroll, row, select, textInput } from './ui';
import { dialOwning, quickDials, type QuickDial, type Tokens } from './theme-quick';
import type { FormSummary, Theme, ThemeToken } from './types';

Expand Down Expand Up @@ -97,6 +97,33 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
}
}, 180 );

/**
* Paints tokens straight onto the previewed form, ahead of the server.
*
* The server render is the truth — it resolves filters and sanitises — but
* it is also a debounce plus a round trip away, and a control that does
* nothing for a third of a second reads as a control that does nothing.
* Every token is a custom property scoped to the form's wrapper, so the
* same value written inline shows the change the moment it is clicked; the
* next `repaint()` replaces the markup wholesale and reconciles whatever
* the server thinks better.
*/
const paintNow = ( written: Record< string, string > ) => {
const wrap = preview.querySelector< HTMLElement >( '.atf-form-wrap' );

if ( ! wrap ) {
return;
}

for ( const [ token, value ] of Object.entries( written ) ) {
if ( '' === value ) {
wrap.style.removeProperty( `--atf-${ token }` );
} else {
wrap.style.setProperty( `--atf-${ token }`, value );
}
}
};

/** The resolved value of a token right now: override, theme, then default. */
const resolve = ( token: ThemeToken ): string => {
if ( overrides[ token.token ] !== undefined ) {
Expand Down Expand Up @@ -152,6 +179,10 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
replaceOverrides( {} );

options.onTheme( active );
// The switched-to theme's full token set, inline, so the
// preview wears it this frame rather than after the
// round trip.
paintNow( theme.resolved );
renderThemes();
renderQuick();
renderControls();
Expand Down Expand Up @@ -186,6 +217,7 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
options.onOverride( token, value );
}

paintNow( written );
renderQuick();
renderControls();
repaint();
Expand Down Expand Up @@ -314,6 +346,7 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
}

options.onOverride( token.token, next );
paintNow( { [ token.token ]: next } );
repaint();
};

Expand Down Expand Up @@ -540,6 +573,7 @@ export function mountThemeControls( options: ThemeControlsOptions ): HTMLElement
options.onOverride( name, String( value ) );
}

paintNow( { ...overrides } );
renderControls();
repaint();
} catch ( error ) {
Expand Down Expand Up @@ -661,6 +695,7 @@ export async function mountThemeStudio(): Promise< void > {
}

root.dataset.atfsMounted = '1';
pinWindowBodyScroll( root );

const bar = root.querySelector< HTMLElement >( '[data-atfs-bar]' );
const body = root.querySelector< HTMLElement >( '.atfs__body' ) ?? root;
Expand Down
41 changes: 41 additions & 0 deletions src/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,44 @@ export function writeSetting( key: string, value: string ): void {
// Storage unavailable; the preference lasts this session only.
}
}

/**
* Keeps the shell window body this root lives in from ever scrolling.
*
* A native window's body is `overflow: auto` in the shell, and under some
* sequences of tab switches and window resizes the browser decides it has
* scrollable overflow even though every pane in here manages its own. One
* stray wheel tick — or a `scrollIntoView` from focusing a control — then
* shunts the whole tool upward: the toolbar disappears under the title bar and
* an equal band of dead space opens at the bottom, which reads as a broken
* window rather than as a scrolled one. Nothing in these windows ever wants
* the body scrolled, so any scroll that happens is undone on arrival.
*
* A stylesheet rule blocks the wheel; this catches the programmatic scrolls
* CSS cannot.
*/
export function pinWindowBodyScroll( root: HTMLElement ): void {
const body = root.closest< HTMLElement >( '.os-window__body' );

if ( ! body ) {
return;
}

body.addEventListener(
'scroll',
() => {
if ( body.scrollTop ) {
body.scrollTop = 0;
}

if ( body.scrollLeft ) {
body.scrollLeft = 0;
}
},
{ passive: true }
);

// Undo anything that happened before this listener existed.
body.scrollTop = 0;
body.scrollLeft = 0;
}
Loading