feat: navigation clavier et barre de formule dans la visionneuse XLSX #153
A7 : barre de formule sous la barre d'outils (nom de cellule active A1 + miroir du contenu, édition live, Entrée valide, Échap annule puis refocalise) ; flèches et Tab déplacent la cellule active dans les 4 directions (Shift inverse Tab), outline persistant sur la cellule active quand le focus passe à la barre ; Maj+Entrée ne valide plus (réservé au multiligne A7+). td.tabIndex=0 pour la focalisation clavier. Vérifié : xlsx-viewer.test.mjs 25/25 (6 nouveaux), E2E 9/9 (barre + flèches + Échap sur fixture réelle), validate-imports, i18n parity (xlsx.formula_bar_placeholder, xlsx.active_cell). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <[email protected]>
This commit is contained in:
@@ -231,4 +231,28 @@ test.describe('Excel viewer — troncature et navigation (#153 A8/A9)', () => {
|
||||
// Tout est chargé → le pied de page est masqué.
|
||||
await expect(foot).toBeHidden();
|
||||
});
|
||||
|
||||
test('barre de formule et navigation clavier (#153 A7)', async ({ page }) => {
|
||||
await login(page);
|
||||
await openXlsx(page, LARGE);
|
||||
|
||||
// Un clic sur une cellule active la barre avec son nom et son contenu.
|
||||
const cell = page.locator('#content-area td[data-cell="B2"]');
|
||||
await cell.click();
|
||||
await expect(page.locator('#xlsx-active-cell')).toHaveText('B2');
|
||||
const bar = page.locator('#xlsx-formula-input');
|
||||
await expect(bar).toHaveValue(/Operation 1/);
|
||||
|
||||
// Les flèches déplacent la cellule active.
|
||||
await cell.press('ArrowDown');
|
||||
await expect(page.locator('#xlsx-active-cell')).toHaveText('B3');
|
||||
await page.locator('#content-area td[data-cell="B3"]').press('ArrowRight');
|
||||
await expect(page.locator('#xlsx-active-cell')).toHaveText('C3');
|
||||
|
||||
// Éditer depuis la barre marque la cellule dirty, Échap annule.
|
||||
await bar.fill('Operation 2 modifiee');
|
||||
await expect(page.locator('#content-area td[data-cell="C3"]')).toHaveClass(/xlsx-dirty/);
|
||||
await page.locator('#content-area td[data-cell="C3"]').press('Escape');
|
||||
await expect(page.locator('#content-area td.xlsx-dirty')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* - A9bis : the tail of a truncated sheet is fetched window by window from
|
||||
* GET …/xlsx/sheet (scroll sentinel + click), and the appended rows are
|
||||
* editable like the initial ones.
|
||||
* - A7 : formula bar mirrors the active cell; arrows/Tab navigate; editing
|
||||
* from the bar marks the cell dirty; Escape reverts.
|
||||
*
|
||||
* Usage: node tests/frontend/xlsx-viewer.test.mjs
|
||||
*/
|
||||
@@ -121,8 +123,9 @@ await initI18n();
|
||||
|
||||
const sheetHtml = (value) =>
|
||||
'<div class="csv-table-wrapper"><table class="csv-table xlsx-table">' +
|
||||
'<thead><tr><th class="xlsx-corner"></th><th>A</th></tr></thead><tbody>' +
|
||||
`<tr><th class="xlsx-rownum">1</th><td data-cell="A1">${value}</td></tr>` +
|
||||
'<thead><tr><th class="xlsx-corner"></th><th>A</th><th>B</th></tr></thead><tbody>' +
|
||||
`<tr><th class="xlsx-rownum">1</th><td data-cell="A1">${value}</td><td data-cell="B1">B1</td></tr>` +
|
||||
'<tr><th class="xlsx-rownum">2</th><td data-cell="A2">A2</td><td data-cell="B2">B2</td></tr>' +
|
||||
"</tbody></table></div>";
|
||||
|
||||
// The JSDOM fetch double serves locale files; everything else is a recorded
|
||||
@@ -414,6 +417,80 @@ await test("a failed window fetch keeps the footnote and shows an error toast",
|
||||
assert.equal(area.querySelector(".xlsx-load-more").classList.contains("done"), true);
|
||||
});
|
||||
|
||||
// ── A7 — formula bar & keyboard navigation ──────────────────────────────────
|
||||
|
||||
await test("the formula bar starts empty and disabled", () => {
|
||||
const area = mount();
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "—");
|
||||
assert.equal(area.querySelector("#xlsx-formula-input").disabled, true);
|
||||
});
|
||||
|
||||
await test("focusing a cell shows its name and content in the bar", () => {
|
||||
const area = mount();
|
||||
const td = area.querySelector('td[data-cell="A1"]');
|
||||
td.dispatchEvent(new w.Event("focus", { bubbles: false }));
|
||||
// JSDOM does not run the default focus behaviour on dispatchEvent, so go
|
||||
// through the real API:
|
||||
td.focus();
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1");
|
||||
assert.equal(area.querySelector("#xlsx-formula-input").value, "100");
|
||||
assert.equal(area.querySelector("#xlsx-formula-input").disabled, false);
|
||||
});
|
||||
|
||||
await test("typing in the bar edits the cell live and marks it dirty", () => {
|
||||
const area = mount();
|
||||
const td = area.querySelector('td[data-cell="A1"]');
|
||||
td.focus();
|
||||
const input = area.querySelector("#xlsx-formula-input");
|
||||
input.value = "depuis la barre";
|
||||
input.dispatchEvent(new w.Event("input", { bubbles: true }));
|
||||
assert.equal(td.textContent, "depuis la barre");
|
||||
assert.equal(td.classList.contains("xlsx-dirty"), true);
|
||||
assert.equal(area.querySelector("#xlsx-save-btn").disabled, false);
|
||||
// The save payload carries the cell edit.
|
||||
area.querySelector("#xlsx-save-btn").click();
|
||||
return new Promise((r) => setTimeout(r, 5)).then(() => {
|
||||
assert.deepEqual(calls[0].body.cells, { A1: "depuis la barre" });
|
||||
});
|
||||
});
|
||||
|
||||
await test("Tab and arrows move to the neighbour cell", () => {
|
||||
const area = mount();
|
||||
const a1 = area.querySelector('td[data-cell="A1"]');
|
||||
a1.focus();
|
||||
a1.dispatchEvent(new w.KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "B1");
|
||||
const b1 = area.querySelector('td[data-cell="B1"]');
|
||||
b1.dispatchEvent(new w.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "B2");
|
||||
const b2 = area.querySelector('td[data-cell="B2"]');
|
||||
b2.dispatchEvent(new w.KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A2");
|
||||
});
|
||||
|
||||
await test("Enter commits and Shift+Tab goes backwards", () => {
|
||||
const area = mount();
|
||||
const a1 = area.querySelector('td[data-cell="A1"]');
|
||||
a1.focus();
|
||||
a1.dispatchEvent(new w.KeyboardEvent("keydown", { key: "Tab", bubbles: true }));
|
||||
const b1 = area.querySelector('td[data-cell="B1"]');
|
||||
b1.dispatchEvent(new w.KeyboardEvent("keydown", { key: "Tab", shiftKey: true, bubbles: true }));
|
||||
assert.equal(area.querySelector("#xlsx-active-cell").textContent, "A1");
|
||||
});
|
||||
|
||||
await test("a saved edit from the bar resets the dirty flag and orig value", async () => {
|
||||
const area = mount();
|
||||
const td = area.querySelector('td[data-cell="A1"]');
|
||||
td.focus();
|
||||
const input = area.querySelector("#xlsx-formula-input");
|
||||
input.value = "200";
|
||||
input.dispatchEvent(new w.Event("input", { bubbles: true }));
|
||||
area.querySelector("#xlsx-save-btn").click();
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
assert.equal(area.querySelectorAll("td.xlsx-dirty").length, 0);
|
||||
assert.equal(td.dataset.orig, "200");
|
||||
});
|
||||
|
||||
await test("two windows in a row walk the whole sheet", async () => {
|
||||
const area = mountTruncated({ total: 1200 });
|
||||
apiQueue.push({
|
||||
|
||||
Reference in New Issue
Block a user