mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(mobile): keep a long prompt visible instead of hiding it behind the keyboard
Typing a prompt long enough to wrap ran the text off the bottom of the screen: the tail — the part being typed, where the cursor is — sat behind the on-screen keyboard, so the user was typing blind. Two independent causes. **The overlay had no bottom bound.** On touch devices keystrokes are buffered in the local-echo overlay and do not reach the PTY until Enter, so the CLI never learns the prompt is long and nothing scrolls or reflows to make room. Meanwhile the renderer lays its wrapped lines out straight DOWNWARD from the prompt row (`top = promptRow * cellH`, each line at `i * cellH`) with nothing clamping it to the visible rows — and with the keyboard up there are only a handful of those. The block now grows UPWARD once it would pass the last visible row: it is lifted so its final line lands ON that row. Every line div is opaque, so it covers transcript above rather than vanishing under the keyboard below — the same thing a real terminal does when a composer expands. A prompt taller than the whole viewport keeps its TAIL, for the same reason the fix exists: the end is what the user is looking at. `startCol` indents only the line that starts at the prompt marker, so it is dropped along with that line when only the tail fits, and the cursor follows the last VISIBLE line. `rows` joins the render key: the layout depends on it, so a keyboard opening — which changes rows without changing the text — must not be skipped as a redundant render. **`_shrinkPaddingToFit()` was reclaiming the bars' own space.** On phones the toolbar and accessory bar are `position: fixed`, so they occupy no layout space and `main`'s padding-bottom is the ONLY thing reserving room for them. Shrinking it by the full sub-row slack pulled the terminal's bottom edge down underneath them, and the row the following re-fit gained was painted behind them — clipping the last line of a long prompt. The shrink now has a floor: the MEASURED height of the currently-visible fixed bars, so genuine over-reservation of the hard-coded 84px is still reclaimed while a device that needs those pixels keeps them. The floor is `Math.min(currentPadding, measured)`, so it can only ever prevent a shrink, never cause a grow that would resize the terminal as a side effect. Overlay behaviour lives in `packages/xterm-zerolag-input/` (single-source; the vendor bundles are generated), so the fix is in the package with the row count passed in as an optional `totalRows` — absent, the layout is exactly as before. Tests: 7 cases in the package's `overlay-renderer.test.ts` (upward lift, tail retention, indent drop, cursor on the last visible line, and the unclamped fallbacks) and 7 in a new `test/mobile-keyboard-bottom-padding.test.ts` (reclaim, floor, partial reclaim, no-grow, hidden bars, CJK strip, whole-row slack). 5 and 4 of them respectively fail without the fix. Package suite 238 pass, including the codex byte-identity and replay tests. Verified on Android + Chrome against a live instance: a ~460-character prompt wrapping ~12 rows stays on screen while typing and arrives at the PTY intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
756728e553
commit
ba843bb272
@@ -65,38 +65,71 @@ export function renderOverlay(container: HTMLDivElement, params: RenderParams):
|
||||
charTop,
|
||||
charHeight,
|
||||
promptRow,
|
||||
totalRows,
|
||||
font,
|
||||
showCursor,
|
||||
cursorColor,
|
||||
terminal,
|
||||
} = params;
|
||||
|
||||
// Position container at prompt row.
|
||||
// ── Keep what is being typed ON SCREEN ────────────────────────────
|
||||
//
|
||||
// The overlay lays its wrapped lines out DOWNWARD from the prompt row, and
|
||||
// nothing past the last terminal row is visible. On a phone the strip left
|
||||
// above the on-screen keyboard is only a handful of rows, so a prompt long
|
||||
// enough to wrap ran off the bottom and the user was typing blind — the tail
|
||||
// of their own sentence, the part they are actually looking at, hidden behind
|
||||
// the keyboard.
|
||||
//
|
||||
// So the composer grows UPWARD once it reaches the last row, exactly as a real
|
||||
// terminal's does: every line div is opaque (see makeLine), so the lines cover
|
||||
// transcript rows above instead of vanishing under the keyboard below, and the
|
||||
// newest text stays where the eye is. A prompt taller than the whole viewport
|
||||
// keeps its TAIL for the same reason.
|
||||
//
|
||||
// `startCol` indents only the line that begins at the prompt marker, so it is
|
||||
// dropped along with that line when the tail is all that fits.
|
||||
const rows = totalRows && totalRows > 0 ? totalRows : terminal?.rows;
|
||||
let visibleLines = lines;
|
||||
let keepsPromptLine = true;
|
||||
let topRow = promptRow;
|
||||
if (rows && rows > 0) {
|
||||
if (lines.length > rows) {
|
||||
visibleLines = lines.slice(lines.length - rows);
|
||||
keepsPromptLine = false;
|
||||
topRow = 0;
|
||||
} else if (promptRow + lines.length > rows) {
|
||||
topRow = rows - lines.length;
|
||||
}
|
||||
}
|
||||
topRow = Math.max(0, topRow);
|
||||
|
||||
container.style.left = '0px';
|
||||
container.style.top = promptRow * cellH + 'px';
|
||||
container.style.top = topRow * cellH + 'px';
|
||||
|
||||
// Clear and rebuild (typically 1-3 line divs, negligible cost)
|
||||
container.innerHTML = '';
|
||||
const fullWidthPx = totalCols * cellW;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const leftPx = i === 0 ? startCol * cellW : 0;
|
||||
const widthPx = i === 0 ? fullWidthPx - leftPx : fullWidthPx;
|
||||
for (let i = 0; i < visibleLines.length; i++) {
|
||||
const indents = i === 0 && keepsPromptLine;
|
||||
const leftPx = indents ? startCol * cellW : 0;
|
||||
const widthPx = indents ? fullWidthPx - leftPx : fullWidthPx;
|
||||
const topPx = i * cellH;
|
||||
const lineEl = makeLine(lines[i], leftPx, topPx, widthPx, cellH, cellW, charTop, charHeight, font, terminal);
|
||||
const lineEl = makeLine(visibleLines[i], leftPx, topPx, widthPx, cellH, cellW, charTop, charHeight, font, terminal);
|
||||
container.appendChild(lineEl);
|
||||
}
|
||||
|
||||
// Block cursor at end of last line (use visual width for CJK support)
|
||||
if (showCursor) {
|
||||
const lastLine = lines[lines.length - 1];
|
||||
const lastLineLeft = lines.length === 1 ? startCol : 0;
|
||||
const lastLine = visibleLines[visibleLines.length - 1];
|
||||
const lastLineLeft = visibleLines.length === 1 && keepsPromptLine ? startCol : 0;
|
||||
const cursorCol = lastLineLeft + stringCellWidth(terminal, lastLine);
|
||||
if (cursorCol < totalCols) {
|
||||
const cursor = document.createElement('span');
|
||||
cursor.style.cssText = 'position:absolute;display:inline-block';
|
||||
cursor.style.left = cursorCol * cellW + 'px';
|
||||
cursor.style.top = (lines.length - 1) * cellH + 'px';
|
||||
cursor.style.top = (visibleLines.length - 1) * cellH + 'px';
|
||||
cursor.style.width = cellW + 'px';
|
||||
cursor.style.height = cellH + 'px';
|
||||
cursor.style.backgroundColor = cursorColor;
|
||||
|
||||
@@ -172,6 +172,13 @@ export interface RenderParams {
|
||||
/** Height of the character rendering area (px). */
|
||||
charHeight: number;
|
||||
promptRow: number;
|
||||
/**
|
||||
* Visible terminal rows. When given, the overlay is kept ON SCREEN: it grows
|
||||
* upward instead of running off the bottom edge, and a wrapped prompt taller
|
||||
* than the viewport keeps its tail. Omit to lay out straight down from
|
||||
* `promptRow` (the historical behaviour).
|
||||
*/
|
||||
totalRows?: number;
|
||||
font: FontStyle;
|
||||
showCursor: boolean;
|
||||
cursorColor: string;
|
||||
|
||||
@@ -565,7 +565,10 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
|
||||
// Skip redundant re-renders — include text content to detect
|
||||
// same-length changes (e.g., setFlushed with different text)
|
||||
const renderKey = `${displayText}:${startCol}:${activePrompt.row}:${activePrompt.col}:${totalCols}:${this._flushedOffset}`;
|
||||
// `rows` is part of the key: the layout is clamped to the visible rows
|
||||
// (see renderOverlay), so a keyboard opening — which changes rows without
|
||||
// changing the text — must not be skipped as a redundant render.
|
||||
const renderKey = `${displayText}:${startCol}:${activePrompt.row}:${activePrompt.col}:${totalCols}:${this._terminal.rows}:${this._flushedOffset}`;
|
||||
if (renderKey === this._lastRenderKey && this._overlay.style.display !== 'none') return;
|
||||
this._lastRenderKey = renderKey;
|
||||
|
||||
@@ -612,6 +615,7 @@ export class ZerolagInputAddon implements XtermAddon {
|
||||
charTop,
|
||||
charHeight,
|
||||
promptRow: activePrompt.row,
|
||||
totalRows: this._terminal.rows,
|
||||
font: this._font,
|
||||
showCursor: this._options.showCursor,
|
||||
cursorColor,
|
||||
|
||||
@@ -418,3 +418,88 @@ describe('stringCellWidth', () => {
|
||||
expect(stringCellWidth(null, '')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderOverlay — staying on screen (totalRows)', () => {
|
||||
// A phone with the keyboard up leaves only a handful of terminal rows. The
|
||||
// overlay lays its wrapped lines out downward from the prompt row, so a long
|
||||
// prompt used to run off the bottom edge and the user typed blind, with the
|
||||
// tail of their own sentence behind the keyboard. With totalRows known, the
|
||||
// composer grows UPWARD instead — the line divs are opaque, so they cover
|
||||
// transcript above rather than disappearing below.
|
||||
const linesOf = (n: number) => Array.from({ length: n }, (_, i) => `line${i}`);
|
||||
const lineDivs = (container: HTMLDivElement) =>
|
||||
Array.from(container.children).filter((el) => el.tagName === 'DIV') as HTMLDivElement[];
|
||||
|
||||
it('lifts the block so its last line lands on the last visible row', () => {
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(container, makeParams({ lines: linesOf(5), promptRow: 10, totalRows: 12, cellH: 17 }));
|
||||
|
||||
// 10 + 5 would end on row 14 of a 12-row screen; the block starts at 7 instead.
|
||||
expect(container.style.top).toBe(7 * 17 + 'px');
|
||||
expect(lineDivs(container)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('leaves the prompt row alone when the block already fits', () => {
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(container, makeParams({ lines: linesOf(3), promptRow: 5, totalRows: 24, cellH: 17 }));
|
||||
|
||||
expect(container.style.top).toBe(5 * 17 + 'px');
|
||||
});
|
||||
|
||||
it('keeps the TAIL when the prompt is taller than the whole viewport', () => {
|
||||
// The end is where the cursor is, and where the user is looking.
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(container, makeParams({ lines: linesOf(6), promptRow: 2, totalRows: 3, cellH: 20 }));
|
||||
|
||||
const divs = lineDivs(container);
|
||||
expect(container.style.top).toBe('0px');
|
||||
expect(divs).toHaveLength(3);
|
||||
expect(divs.map((d) => d.textContent)).toEqual(['line3', 'line4', 'line5']);
|
||||
});
|
||||
|
||||
it('drops the prompt indent once the prompt line is no longer shown', () => {
|
||||
// startCol indents only the line that begins at the prompt marker.
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(
|
||||
container,
|
||||
makeParams({ lines: linesOf(6), promptRow: 2, totalRows: 3, startCol: 5, cellW: 10, totalCols: 80 })
|
||||
);
|
||||
|
||||
const first = lineDivs(container)[0];
|
||||
expect(first.style.left).toBe('0px');
|
||||
expect(first.style.width).toBe(80 * 10 + 'px');
|
||||
});
|
||||
|
||||
it('rides the cursor on the last VISIBLE line', () => {
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(
|
||||
container,
|
||||
makeParams({ lines: ['aaa', 'bbb', 'ccc', 'ddd'], promptRow: 9, totalRows: 3, cellH: 20, cellW: 10, startCol: 4 })
|
||||
);
|
||||
|
||||
const cursor = Array.from(container.children).find((el) => el.tagName === 'SPAN') as HTMLSpanElement;
|
||||
// Tail is the last 3 lines, so the cursor sits on row 2 (0-based) of the block…
|
||||
expect(cursor.style.top).toBe(2 * 20 + 'px');
|
||||
// …at column 3, NOT startCol + 3: the indented prompt line is not shown.
|
||||
expect(cursor.style.left).toBe(3 * 10 + 'px');
|
||||
});
|
||||
|
||||
it('lays out straight down when totalRows is absent (unchanged behaviour)', () => {
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(container, makeParams({ lines: linesOf(9), promptRow: 20, cellH: 17 }));
|
||||
|
||||
expect(container.style.top).toBe(20 * 17 + 'px');
|
||||
expect(lineDivs(container)).toHaveLength(9);
|
||||
});
|
||||
|
||||
it('falls back to the terminal row count when totalRows is not passed', () => {
|
||||
// The addon passes totalRows, but a stale bundle / third-party caller may not.
|
||||
const container = document.createElement('div');
|
||||
renderOverlay(
|
||||
container,
|
||||
makeParams({ lines: linesOf(4), promptRow: 8, cellH: 17, terminal: { rows: 10, cols: 80 } as never })
|
||||
);
|
||||
|
||||
expect(container.style.top).toBe(6 * 17 + 'px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,6 +573,42 @@ const KeyboardHandler = {
|
||||
* space below the last row. After fitAddon.fit(), measure the gap and
|
||||
* reduce padding by that amount so the terminal sits flush against the bars.
|
||||
*/
|
||||
/**
|
||||
* Combined height of the fixed bars that overlay the terminal's bottom edge.
|
||||
*
|
||||
* On phones the toolbar and the accessory bar are `position: fixed`, so they
|
||||
* occupy no layout space of their own — `main`'s padding-bottom is the only
|
||||
* thing reserving room for them, and any pixel taken out of it is a pixel of
|
||||
* terminal painted underneath them.
|
||||
*/
|
||||
_fixedBottomBarsHeight() {
|
||||
let px = 0;
|
||||
for (const selector of ['.toolbar', '.keyboard-accessory-bar', '#cjkInput.cjk-input-visible']) {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) continue;
|
||||
const style = window.getComputedStyle?.(el);
|
||||
if (style && (style.display === 'none' || style.visibility === 'hidden')) continue;
|
||||
px += el.offsetHeight || 0;
|
||||
}
|
||||
return px;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reclaim sub-row slack at the bottom of the terminal — but never the space the
|
||||
* fixed bars stand in.
|
||||
*
|
||||
* Shrinking the padding by the whole slack pulled the terminal's bottom edge
|
||||
* DOWN under those bars, and the row the following re-fit then gained was
|
||||
* painted behind them: on a long wrapped prompt the last line was clipped by
|
||||
* the accessory bar, i.e. the bottom half of the text being typed. The floor is
|
||||
* now the bars' MEASURED height, so a device where the hard-coded 84px
|
||||
* over-reserves still reclaims the difference, while one that genuinely needs
|
||||
* it keeps every pixel.
|
||||
*
|
||||
* ⚠️ The floor can only ever prevent a shrink, never cause a grow
|
||||
* (`Math.min(currentPadding, …)`): a measured height LARGER than the current
|
||||
* padding makes this a no-op rather than silently resizing the terminal.
|
||||
*/
|
||||
_shrinkPaddingToFit() {
|
||||
try {
|
||||
const container = document.getElementById('terminalContainer');
|
||||
@@ -583,7 +619,8 @@ const KeyboardHandler = {
|
||||
const gap = container.clientHeight - app.terminal.rows * cellH;
|
||||
if (gap > 0 && gap < cellH) {
|
||||
const currentPadding = parseInt(main.style.paddingBottom) || 0;
|
||||
main.style.paddingBottom = Math.max(0, currentPadding - gap) + 'px';
|
||||
const floor = Math.min(currentPadding, this._fixedBottomBarsHeight());
|
||||
main.style.paddingBottom = Math.max(floor, currentPadding - gap) + 'px';
|
||||
if (app.fitAddon)
|
||||
try {
|
||||
app.fitAddon.fit();
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Port: none (pure logic in a vm context — no browser, no server).
|
||||
//
|
||||
// On phones the toolbar and the keyboard accessory bar are `position: fixed`, so
|
||||
// they take no layout space: `main`'s padding-bottom is the ONLY thing reserving
|
||||
// room for them, and every pixel taken out of it is a pixel of terminal painted
|
||||
// underneath them.
|
||||
//
|
||||
// `_shrinkPaddingToFit` reclaims the sub-row slack left after a keyboard-driven
|
||||
// re-fit. It used to take the whole slack, which pulled the terminal's bottom edge
|
||||
// down under those bars — and the row the following re-fit gained was painted
|
||||
// behind them, clipping the last line of a long wrapped prompt: the bottom half of
|
||||
// the text being typed. The floor is now the bars' MEASURED height.
|
||||
//
|
||||
// Lives outside test/mobile/ deliberately — that suite is Playwright-driven and
|
||||
// excluded from `npm run test:ci`, so a regression guarded only there is invisible
|
||||
// to CI (same reasoning as terminal-scroll-intent.test.ts).
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const SOURCE = readFileSync(resolve(import.meta.dirname, '../src/web/public/mobile-handlers.js'), 'utf8');
|
||||
|
||||
interface Bar {
|
||||
offsetHeight: number;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
interface Setup {
|
||||
paddingBottom: string;
|
||||
containerHeight: number;
|
||||
rows: number;
|
||||
cellH: number;
|
||||
bars: Partial<Record<'.toolbar' | '.keyboard-accessory-bar' | '#cjkInput.cjk-input-visible', Bar>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load mobile-handlers.js and hand back its KeyboardHandler plus the fake `main`
|
||||
* whose inline padding the function edits.
|
||||
*
|
||||
* `const KeyboardHandler = {...}` is a lexical binding that does not survive to a
|
||||
* second `vm.runInContext`, so the export is appended to the SAME script.
|
||||
*/
|
||||
function loadHandler(setup: Setup) {
|
||||
const main = { style: { paddingBottom: setup.paddingBottom } };
|
||||
const container = { clientHeight: setup.containerHeight };
|
||||
let fits = 0;
|
||||
const app = {
|
||||
terminal: {
|
||||
rows: setup.rows,
|
||||
_core: { _renderService: { dimensions: { css: { cell: { height: setup.cellH } } } } },
|
||||
},
|
||||
fitAddon: {
|
||||
fit: () => {
|
||||
fits++;
|
||||
},
|
||||
},
|
||||
};
|
||||
const context = vm.createContext({
|
||||
console,
|
||||
app,
|
||||
navigator: { userAgent: 'test', maxTouchPoints: 1 },
|
||||
window: {
|
||||
addEventListener: () => {},
|
||||
matchMedia: () => ({ matches: false }),
|
||||
scrollTo: () => {},
|
||||
getComputedStyle: (el: Bar) => ({ display: el.hidden ? 'none' : 'block', visibility: 'visible' }),
|
||||
},
|
||||
document: {
|
||||
body: { classList: { add: () => {}, remove: () => {} } },
|
||||
addEventListener: () => {},
|
||||
getElementById: (id: string) => (id === 'terminalContainer' ? container : null),
|
||||
querySelector: (sel: string) =>
|
||||
sel === '.main' ? main : (setup.bars as Record<string, Bar | undefined>)[sel] || null,
|
||||
},
|
||||
setTimeout: () => 1,
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
vm.runInContext(`${SOURCE}\nglobalThis.__KH = KeyboardHandler;`, context, { filename: 'mobile-handlers.js' });
|
||||
return { handler: (context as { __KH: any }).__KH, main, fits: () => fits };
|
||||
}
|
||||
|
||||
// 10 rows × 19px = 190 in a 200px container → 10px of slack, less than one row.
|
||||
const BASE: Setup = {
|
||||
paddingBottom: '84px',
|
||||
containerHeight: 200,
|
||||
rows: 10,
|
||||
cellH: 19,
|
||||
bars: { '.toolbar': { offsetHeight: 40 }, '.keyboard-accessory-bar': { offsetHeight: 44 } },
|
||||
};
|
||||
|
||||
describe('_shrinkPaddingToFit', () => {
|
||||
it('reclaims the slack when the reservation over-reserves', () => {
|
||||
// Bars really need 60px, 84 is reserved → the 10px of slack is free to take.
|
||||
const { handler, main } = loadHandler({
|
||||
...BASE,
|
||||
bars: { '.toolbar': { offsetHeight: 30 }, '.keyboard-accessory-bar': { offsetHeight: 30 } },
|
||||
});
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('74px');
|
||||
});
|
||||
|
||||
it('never shrinks into the space the bars actually occupy', () => {
|
||||
// 40 + 44 = 84: the reservation is exactly right, so there is nothing to take
|
||||
// even though the terminal has 10px of slack.
|
||||
const { handler, main } = loadHandler(BASE);
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('84px');
|
||||
});
|
||||
|
||||
it('stops part-way when only some of the slack is free', () => {
|
||||
// Bars need 78px of the reserved 84 → 6px may be reclaimed, not the full 10.
|
||||
const { handler, main } = loadHandler({
|
||||
...BASE,
|
||||
bars: { '.toolbar': { offsetHeight: 34 }, '.keyboard-accessory-bar': { offsetHeight: 44 } },
|
||||
});
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('78px');
|
||||
});
|
||||
|
||||
it('is a no-op, never a grow, when the bars are taller than the reservation', () => {
|
||||
// Growing the padding here would resize the terminal as a side effect of a
|
||||
// function that exists to reclaim slack.
|
||||
const { handler, main } = loadHandler({
|
||||
...BASE,
|
||||
bars: { '.toolbar': { offsetHeight: 60 }, '.keyboard-accessory-bar': { offsetHeight: 60 } },
|
||||
});
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('84px');
|
||||
});
|
||||
|
||||
it('does not count a hidden bar', () => {
|
||||
// The accessory bar is display:none until the keyboard opens; counting it
|
||||
// would block a reclaim that is genuinely free.
|
||||
const { handler, main } = loadHandler({
|
||||
...BASE,
|
||||
bars: { '.toolbar': { offsetHeight: 40 }, '.keyboard-accessory-bar': { offsetHeight: 44, hidden: true } },
|
||||
});
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('74px');
|
||||
});
|
||||
|
||||
it('counts the CJK input strip when it is on screen', () => {
|
||||
const { handler, main } = loadHandler({
|
||||
...BASE,
|
||||
bars: {
|
||||
'.toolbar': { offsetHeight: 30 },
|
||||
'.keyboard-accessory-bar': { offsetHeight: 30 },
|
||||
'#cjkInput.cjk-input-visible': { offsetHeight: 20 },
|
||||
},
|
||||
});
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('80px');
|
||||
});
|
||||
|
||||
it('leaves the padding alone when the slack is a whole row or more', () => {
|
||||
// A full row of slack means the re-fit will claim it as a row; padding is not
|
||||
// the lever here.
|
||||
const { handler, main, fits } = loadHandler({ ...BASE, containerHeight: 190 + 19 });
|
||||
|
||||
handler._shrinkPaddingToFit();
|
||||
|
||||
expect(main.style.paddingBottom).toBe('84px');
|
||||
expect(fits()).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user