feat: tableur mobile — barres repliées par défaut, bascule unique #189
CI / lint (push) Successful in 2m42s
CI / security (push) Successful in 1m38s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 1m27s
CI / e2e (push) Successful in 17m12s

Sous 768 px, la barre de menus, le ruban et le groupe Recherche sont
repliés par défaut : la barre de formule (fx), les onglets et Enregistrer
restent visibles et la grille récupère la hauteur d'écran. Un bouton
unique (#xlsx-tools-toggle) dans la barre des feuilles déplie/replie
l'ensemble (aria-expanded, état actif) ; desktop inchangé.

Ergonomie tactile : cibles ≥ 44 px, onglets en défilement horizontal,
champs de la barre de formule en 16 px (anti-zoom iOS), barre de formule
en 2 lignes quand les outils sont ouverts.

Tests : JSDOM xlsx-viewer.test.mjs (+2, 165/165), E2E mobile (repli par
défaut, dépliage, no-overflow ruban, cibles 44 px) ; openXlsx() déplie
automatiquement ≤ 768 px pour les tests desktop.
This commit is contained in:
2026-10-08 15:45:39 -04:00
parent e01e837a2a
commit 36b4962efb
17 changed files with 341 additions and 17 deletions
+36 -2
View File
@@ -72,6 +72,14 @@ async function openXlsx(page, file) {
}
await treeItem.dblclick({ timeout: 5000 });
await expect(page.locator('#content-area .xlsx-table')).toBeVisible({ timeout: 15000 });
// #189 — sous 768px la barre de menus et le ruban sont repliés par défaut.
// Les tests « desktop » qui les manipulent les déplient d'abord ; le test
// mobile dédié ouvre lui-même la feuille pour vérifier l'état replié.
const toggle = page.locator('#xlsx-tools-toggle');
if (await toggle.isVisible().catch(() => false)) {
await toggle.click();
await expect(page.locator('#content-area .xlsx-menubar')).toBeVisible();
}
}
test.describe('Excel viewer — garde-fous d\'écriture et valeurs calculées (#153)', () => {
@@ -513,12 +521,27 @@ test.describe('Excel viewer — menus à icônes et fermeture au focus (#179)',
await expect(page.locator('#xlsx-minibar')).toBeVisible();
});
test('mobile : ruban sans débordement et cibles tactiles 44px', async ({ page, viewport }) => {
test('mobile : barres repliées par défaut, ruban sans débordement et cibles 44px (#189)', async ({ page, viewport }) => {
test.skip((viewport?.width ?? 9999) > 768, 'Mobile viewport required');
await login(page);
await page.evaluate(() => window.TabManager.open('TestVault', FIXTURE));
await page.evaluate((f) => window.TabManager.open('TestVault', f), FIXTURE);
await expect(page.locator('#content-area .xlsx-table')).toBeVisible({ timeout: 15000 });
// #189 — par défaut la barre de menus et le ruban sont repliés : la grille
// garde la hauteur d'écran. La barre de formule, elle, reste visible.
await expect(page.locator('#content-area .xlsx-menubar')).toBeHidden();
await expect(page.locator('#content-area .xlsx-cmdbar')).toBeHidden();
await expect(page.locator('#content-area .xlsx-formula-bar')).toBeVisible();
await expect(page.locator('#content-area .xlsx-sheetbar')).toBeVisible();
const toggle = page.locator('#xlsx-tools-toggle');
await expect(toggle).toBeVisible();
// Un clic déplie menus + ruban (état annoncé par aria-expanded).
await toggle.click();
await expect(page.locator('#content-area .xlsx-menubar')).toBeVisible();
await expect(page.locator('#content-area .xlsx-cmdbar')).toBeVisible();
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
// Le ruban s'enroule au lieu de déborder horizontalement.
const overflow = await page.evaluate(() => {
const bar = document.querySelector('#content-area .xlsx-cmdbar');
@@ -526,6 +549,17 @@ test.describe('Excel viewer — menus à icônes et fermeture au focus (#179)',
});
expect(overflow).toBeLessThanOrEqual(1);
// Cibles tactiles : le basculeur et chaque commande du ruban font 44px.
const toggleBox = await toggle.boundingBox();
expect(toggleBox.height).toBeGreaterThanOrEqual(44);
const ribbonBtnBox = await page.locator('#xs-b').boundingBox();
expect(ribbonBtnBox.height).toBeGreaterThanOrEqual(44);
// Repli : la grille retrouve toute sa hauteur.
await toggle.click();
await expect(page.locator('#content-area .xlsx-menubar')).toBeHidden();
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
// Menu contextuel (clic droit émulé) : tient dans le viewport, entrées ≥ 44px.
await page.locator('#content-area td[data-cell="A1"]').click({ button: 'right' });
const menu = page.locator('.xlsx-context-menu');
+48
View File
@@ -2775,6 +2775,54 @@ await test("a read-only workbook keeps the view menus but drops the editing ones
assert.ok(area.querySelector('[data-xlsx-action="toggle-grid"]'), "Gridlines stay");
});
// ── #189 — mise en page mobile : barres repliées derrière un basculeur ──────
await test("the sheet strip carries the tools toggle that flips the toolbar (#189)", () => {
const area = mountGrid();
const strip = area.querySelector(".xlsx-sheetbar");
const toggle = area.querySelector("#xlsx-tools-toggle");
assert.ok(toggle, "toggle rendered");
assert.equal(toggle.closest(".xlsx-sheetbar"), strip, "it lives in the sheet strip");
assert.equal(toggle.getAttribute("aria-expanded"), "false", "starts collapsed");
const toolbar = area.querySelector(".xlsx-toolbar");
assert.ok(!toolbar.classList.contains("xlsx-tools-open"), "toolbar starts closed");
toggle.click();
assert.ok(toolbar.classList.contains("xlsx-tools-open"), "one click opens the tools");
assert.equal(toggle.getAttribute("aria-expanded"), "true");
assert.ok(toggle.classList.contains("active"), "the toggle shows its own state");
toggle.click();
assert.ok(!toolbar.classList.contains("xlsx-tools-open"), "a second click closes them");
assert.equal(toggle.getAttribute("aria-expanded"), "false");
});
await test("style.css hides menus/ribbon/find by default under 768px and reveals them on .xlsx-tools-open (#189)", () => {
const css = readFileSync(path.join(REPO_ROOT, "frontend", "style.css"), "utf8");
// The desktop default: the toggle itself is out of the way.
assert.match(css, /\.xlsx-tools-toggle\s*\{\s*display:\s*none/, "toggle hidden on desktop");
// Mobile block: collapsed by default… (CRLF-safe slice, from the #189
// comment up to the next top-level rule).
const from = css.indexOf("/* #189");
assert.ok(from > 0, "the #189 mobile block exists after the coarse-pointer rules");
const to = css.indexOf(".xlsx-context-item", from);
assert.ok(to > from, "the #189 block is followed by the next rule");
const block = css.slice(from, to);
for (const cls of [".xlsx-menubar", ".xlsx-cmdbar", ".xlsx-find-group"]) {
const hidden = new RegExp(cls.replace(".", "\\.") + "[^}]*display:\\s*none").test(block)
|| new RegExp(cls.replace(".", "\\.") + ",[\\s\\S]{0,120}?display:\\s*none").test(block);
assert.ok(hidden, `${cls} hidden by default on mobile`);
assert.ok(
block.includes(`.xlsx-toolbar.xlsx-tools-open ${cls}`),
`${cls} revealed by .xlsx-tools-open`,
);
}
// The formula bar always stays, and the open state can wrap it (find group).
assert.ok(block.includes(".xlsx-toolbar.xlsx-tools-open .xlsx-formula-bar"), "formula bar wraps when open");
// Touch: 44px targets, tabs on one scrollable row, 16px inputs (no iOS zoom).
assert.match(block, /min-width:\s*44px/, "44px touch targets");
assert.match(block, /\.xlsx-sheetbar \.xlsx-tabs\s*\{[^}]*overflow-x:\s*auto/, "tabs scroll horizontally");
assert.match(block, /font-size:\s*16px/, "inputs at 16px avoid the iOS focus zoom");
});
// ── Report ──────────────────────────────────────────────────────────────────
console.log(`\n${passCount}/${testCount} tests passed\n`);
process.exit(passCount === testCount ? 0 : 1);