fix(web): separate spreadsheet tab navigation from zoom controls

This commit is contained in:
yudshj
2026-09-23 16:13:21 +08:00
parent 3d6d46b8a6
commit c920174964
7 changed files with 123 additions and 13 deletions
+62 -2
View File
@@ -113,9 +113,18 @@ async function expectExcelLayout(excel: Locator): Promise<void> {
const canvas = node.querySelector('canvas')!.getBoundingClientRect()
const formula = node.querySelector('.fortune-workarea')!.getBoundingClientRect()
const tabs = node.querySelector('.luckysheet-sheet-area')!.getBoundingClientRect()
const scroller = node.querySelector('.fortune-sheettab-container')!.getBoundingClientRect()
const controls = [...node.querySelectorAll('.fortune-sheettab-scroll, .fortune-zoom-button')]
.map(control => control.getBoundingClientRect())
return Math.max(Math.abs(canvas.left - pane.left), Math.abs(canvas.width - pane.width),
Math.abs(canvas.top - formula.bottom), Math.abs(canvas.bottom - tabs.top), Math.abs(tabs.bottom - pane.bottom))
Math.abs(canvas.top - formula.bottom), Math.abs(canvas.bottom - tabs.top), Math.abs(tabs.bottom - pane.bottom),
Math.abs(scroller.left - tabs.left), scroller.right - controls[0]!.left,
...controls.map((control, index) => control.right - (controls[index + 1]?.left ?? tabs.right)))
})).toBeLessThanOrEqual(1)
expect(await excel.locator('.fortune-sheettab-scroll, .fortune-zoom-button').evaluateAll(controls => controls.every((control) => {
const rect = control.getBoundingClientRect()
return control.contains(document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2))
}))).toBe(true)
}
it.skipIf(MODE === 'record').each(['en-US', 'zh-CN'])('fills the spreadsheet pane with read-only controls in %s', async (locale) => {
@@ -138,6 +147,7 @@ it.skipIf(MODE === 'record').each(['en-US', 'zh-CN'])('fills the spreadsheet pan
const longCellText = 'Monthly export/import matrix and category comparison. 各商品类别分月出口/进口矩阵与结构对比图(公式汇总) '.repeat(3)
await Promise.all([
writeFile(join(cwd, 'budget.xlsx'), await excelFixture()),
writeFile(join(cwd, 'meeting.xlsx'), await meetingMinutesFixture()),
writeFile(join(cwd, 'legacy.xls'), xlsFixture()),
writeFile(join(cwd, 'values.csv'), `${longCellText},2\n3,4\n`),
writeFile(join(cwd, 'values.tsv'), '1\t2\n3\t4\n'),
@@ -210,10 +220,60 @@ it.skipIf(MODE === 'record').each(['en-US', 'zh-CN'])('fills the spreadsheet pan
await excel.locator('.fortune-zoom-ratio-item').getByText('100%', { exact: true }).click()
const filesTab = column.locator('[data-dockkit-tab]').filter({ has: page.getByText(locale === 'zh-CN' ? '文件' : 'Files', { exact: true }) })
const preview = column.locator('[data-textpreview-url]')
await openPreviewFile(column, filesTab, preview, 'meeting.xlsx')
const tabScroller = excel.locator('.fortune-sheettab-container-c')
const activeSheet = excel.locator('.luckysheet-sheets-item-active .luckysheet-sheets-item-name')
await expect.poll(() => activeSheet.innerText()).toBe('会议信息')
const meetingCanvas = await canvas.elementHandle()
if (meetingCanvas === null) throw new Error('meeting spreadsheet canvas is unavailable')
await expectExcelLayout(excel)
await excel.locator('.fortune-sheet-overlay').click({ position: { x: 60, y: 40 } })
await expect.poll(() => formula.innerText()).toBe('会议纪要')
await expect.poll(() => selection.innerText()).toBe('A1')
for (const width of [1000, 360, 1000, 360]) {
await layout.evaluate((node, width) => { node.textContent = `[data-sidebar-right-panel] { width: ${width}px !important; }` }, width)
await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(width)
await expectExcelLayout(excel)
expect(await meetingCanvas.evaluate(node => node.isConnected)).toBe(true)
expect(await selection.innerText()).toBe('A1')
await expect.poll(() => excel.locator('.fortune-sheettab-scroll').count()).toBe(width === 360 ? 2 : 0)
}
for (const direction of ['right', 'left'] as const) {
const arrow = excel.locator(`#fortune-sheettab-${direction}scroll`)
for (let step = 0; step < 8; step += 1) {
const { offset, maximum } = await tabScroller.evaluate(node => ({
offset: node.scrollLeft, maximum: node.scrollWidth - node.clientWidth,
}))
const target = direction === 'right' ? Math.min(offset + 150, maximum) : Math.max(offset - 150, 0)
if (Math.abs(target - offset) <= 1) break
await arrow.click()
await expect.poll(() => tabScroller.evaluate(node => node.scrollLeft)).toBeCloseTo(target, 0)
}
const sheetName = direction === 'right' ? '填写说明' : '会议信息'
await excel.locator('.luckysheet-sheets-item-name').getByText(sheetName, { exact: true }).click()
await expect.poll(() => activeSheet.innerText()).toBe(sheetName)
await expectExcelLayout(excel)
await successShot(page, `excel-navigation-${direction}-${locale}`)
}
expect(await tabScroller.evaluate(node => Math.abs(
node.firstElementChild!.getBoundingClientRect().left - node.getBoundingClientRect().left,
))).toBeLessThanOrEqual(1)
await excel.locator('.fortune-zoom-button').first().click()
await expect.poll(() => zoom.innerText()).toBe('90%')
await excel.locator('.fortune-zoom-button').last().click()
await expect.poll(() => zoom.innerText()).toBe('100%')
await zoom.click()
await excel.locator('.fortune-zoom-ratio-item').getByText('150%', { exact: true }).click()
await expect.poll(() => zoom.innerText()).toBe('150%')
expect(await activeSheet.innerText()).toBe('会议信息')
await expectExcelLayout(excel)
await successShot(page, `excel-navigation-zoom-${locale}`)
await layout.evaluate((node) => { node.textContent = '' })
await meetingCanvas.dispose()
for (const colorScheme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme })
await expect.poll(() => page.locator('body').getAttribute('data-ds-dark-theme')).toBe(colorScheme === 'dark' ? '' : null)
for (const name of ['budget.xlsx', 'legacy.xls', 'values.csv', 'values.tsv']) {
for (const name of ['budget.xlsx', 'meeting.xlsx', 'legacy.xls', 'values.csv', 'values.tsv']) {
await openPreviewFile(column, filesTab, preview, name)
await excel.locator('.fortune-sheet-overlay').waitFor()
await expectExcelLayout(excel)
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-sidebar-documentpreview/README.md
README.md: 7354c47c7d7385085156424bdef63a7ab6c2e566
README.zh.md: d6985c7d8ab3650ad0c7410970ad08070c48f65b
README.md: 0571c4537f5ff98d8ec1713c297397da1941780b
README.zh.md: 949e66546b8a4f25a388178dea47f90a8d6d324c
@@ -66,7 +66,7 @@ Initial reads, additional pages, and HTML/PDF/image preparation share the ongoin
<a id="excel-preview"></a>
## Excel preview
Open `.xlsx`, `.xls`, `.csv`, and `.tsv` directly in the browser with worksheet tabs, cell selection, copying, and a read-only formula bar. XLSX retains fonts, solid fills, borders, number formats, rich text, merged cells, row and column sizes, hidden rows/columns/sheets, and frozen headings. XLS retains saved values, formulas, number formats, merges, and available row/column metadata; fonts, borders, and frozen panes are unsupported. Workbooks display saved formula results without recalculating; missing results remain blank, and a compact formula-bar warning marks workbooks whose displayed results may be incomplete or inaccurate. The workbook viewport fills the preview pane and follows its size changes without reloading its sheets or selection. Worksheet tabs and zoom controls remain visible; selection statistics and inactive worksheet menu arrows are hidden. Frozen headings remain fixed while scrolling, without freeze dividers or drag handles. Spreadsheet preview does not call the Office conversion service.
Open `.xlsx`, `.xls`, `.csv`, and `.tsv` directly in the browser with worksheet tabs, cell selection, copying, and a read-only formula bar. XLSX retains fonts, solid fills, borders, number formats, rich text, merged cells, row and column sizes, hidden rows/columns/sheets, and frozen headings. XLS retains saved values, formulas, number formats, merges, and available row/column metadata; fonts, borders, and frozen panes are unsupported. Workbooks display saved formula results without recalculating; missing results remain blank, and a compact formula-bar warning marks workbooks whose displayed results may be incomplete or inaccurate. The workbook viewport fills the preview pane and follows its size changes without reloading its sheets or selection. Worksheet tabs start at the left edge; overflowing tabs scroll with left/right navigation beside a separate zoom control. Selection statistics and inactive worksheet menu arrows are hidden. Frozen headings remain fixed while scrolling, without freeze dividers or drag handles. Spreadsheet preview does not call the Office conversion service.
XLSX preview omits DrawingML parts from an in-memory copy before parsing and ignores worksheet drawing references. The original file and worksheet XML remain unchanged. A notice above the table lists detected charts, images, shapes, and conditional formatting that are not displayed, and recommends opening the workbook in a system application. Files without these detected features have no notice; formula warnings remain separate.
@@ -66,7 +66,7 @@ PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `<img>` 静态
<a id="excel-preview"></a>
## Excel 预览
直接在浏览器中打开 `.xlsx`、`.xls`、`.csv` 和 `.tsv`,支持工作表标签、单元格选择、复制和只读公式栏。XLSX 保留字体、纯色填充、边框、数字格式、富文本、合并单元格、行列尺寸、隐藏行列及工作表,以及冻结标题。XLS 保留已保存的值、公式、数字格式、合并及可用的行列元数据;不支持字体、边框和冻结窗格。工作簿显示已保存的公式结果而不重新计算;缺少的结果保持为空,公式栏旁的紧凑提示会标记显示结果可能不完整或不准确的工作簿。工作簿视图填满预览面板,并随面板尺寸变化自动调整,无需重新加载工作表或选择状态。保留工作表标签和缩放控件,隐藏选区统计栏及无效的工作表菜单箭头。冻结标题在滚动时保持固定,不显示冻结分隔线和拖拽条。表格预览不调用 Office 转换服务。
直接在浏览器中打开 `.xlsx`、`.xls`、`.csv` 和 `.tsv`,支持工作表标签、单元格选择、复制和只读公式栏。XLSX 保留字体、纯色填充、边框、数字格式、富文本、合并单元格、行列尺寸、隐藏行列及工作表,以及冻结标题。XLS 保留已保存的值、公式、数字格式、合并及可用的行列元数据;不支持字体、边框和冻结窗格。工作簿显示已保存的公式结果而不重新计算;缺少的结果保持为空,公式栏旁的紧凑提示会标记显示结果可能不完整或不准确的工作簿。工作簿视图填满预览面板,并随面板尺寸变化自动调整,无需重新加载工作表或选择状态。工作表标签从左侧边缘开始排列;标签溢出时可用左右按钮滚动,缩放控件独立占位。隐藏选区统计栏及无效的工作表菜单箭头。冻结标题在滚动时保持固定,不显示冻结分隔线和拖拽条。表格预览不调用 Office 转换服务。
XLSX 预览在解析前从内存副本中移除 DrawingML 部件,并忽略工作表的绘图引用。原文件和工作表 XML 保持不变。表格上方的提示列出实际检测到但未展示的图表、图片、形状和条件格式,并建议使用系统应用打开工作簿。未检测到这些内容的文件不显示此提示;公式提示保持独立。
@@ -28,10 +28,26 @@
padding-right: 5px;
}
.workbook :global(.luckysheet-sheet-area) {
padding: 0;
}
.workbook :global(#luckysheet-sheet-content) {
flex: 1;
min-width: 0;
}
.workbook :global(.fortune-sheettab-container) {
flex: 1;
min-width: 0;
max-width: 100%;
}
.workbook :global(.fortune-sheettab-scroll),
.workbook :global(.fortune-sheet-area-right) {
flex: none;
}
.workbook :global(.fortune-fx-input-container) {
min-width: 0;
}
+38 -4
View File
@@ -1,5 +1,5 @@
diff --git a/dist/index.esm.js b/dist/index.esm.js
index c4486a2b38bec4eff10cdfda4d3748d40bfb71ba..c76ab678d776641131f582a194b5008a73a19bf2 100644
index c4486a2b38bec4eff10cdfda4d3748d40bfb71ba..c5a6ee9adf10cdf8ad8dd7fde39d56aa67644c26 100644
--- a/dist/index.esm.js
+++ b/dist/index.esm.js
@@ -921,7 +921,7 @@ var InputBox = function InputBox() {
@@ -40,7 +40,24 @@ index c4486a2b38bec4eff10cdfda4d3748d40bfb71ba..c76ab678d776641131f582a194b5008a
}
draftCtx.luckysheet_selection_range = [];
});
@@ -10693,7 +10698,8 @@ var SheetListItem = function SheetListItem(_ref) {
@@ -7248,8 +7253,14 @@ var SheetTab = function SheetTab() {
useEffect(function () {
var tabCurrent = tabContainerRef.current;
if (!tabCurrent) return;
- setIsShowScrollBtn(tabCurrent.scrollWidth - 2 > tabCurrent.clientWidth);
- }, [context.luckysheetfile]);
+ var availableWidth = tabCurrent.clientWidth;
+ if (context.allowEdit === false) {
+ // Count navigation space before deciding whether the tabs need it.
+ availableWidth += leftScrollRef.current ? leftScrollRef.current.offsetWidth : 0;
+ availableWidth += rightScrollRef.current ? rightScrollRef.current.offsetWidth : 0;
+ }
+ setIsShowScrollBtn(tabCurrent.scrollWidth - 2 > availableWidth);
+ }, [context.luckysheetfile, context.allowEdit, context.luckysheetTableContentHW[0]]);
var onAddSheetClick = useCallback(function () {
return setTimeout(function () {
setContext(function (draftCtx) {
@@ -10693,7 +10704,8 @@ var SheetListItem = function SheetListItem(_ref) {
draftCtx.scrollLeft = 0;
draftCtx.scrollTop = 0;
draftCtx.luckysheet_select_status = false;
@@ -51,7 +68,7 @@ index c4486a2b38bec4eff10cdfda4d3748d40bfb71ba..c76ab678d776641131f582a194b5008a
draftCtx.luckysheet_selection_range = [];
});
diff --git a/dist/index.js b/dist/index.js
index e4045518773578e3faaa3a58985a5585f9683088..e559e7a600365993a2994746c84e56f64bbf0033 100644
index e4045518773578e3faaa3a58985a5585f9683088..2b217b8dde18934a020ef8ba1f2dd5c161c68b7f 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -932,7 +932,7 @@ var InputBox = function InputBox() {
@@ -92,7 +109,24 @@ index e4045518773578e3faaa3a58985a5585f9683088..e559e7a600365993a2994746c84e56f6
}
draftCtx.luckysheet_selection_range = [];
});
@@ -10704,7 +10709,8 @@ var SheetListItem = function SheetListItem(_ref) {
@@ -7259,8 +7264,14 @@ var SheetTab = function SheetTab() {
React.useEffect(function () {
var tabCurrent = tabContainerRef.current;
if (!tabCurrent) return;
- setIsShowScrollBtn(tabCurrent.scrollWidth - 2 > tabCurrent.clientWidth);
- }, [context.luckysheetfile]);
+ var availableWidth = tabCurrent.clientWidth;
+ if (context.allowEdit === false) {
+ // Count navigation space before deciding whether the tabs need it.
+ availableWidth += leftScrollRef.current ? leftScrollRef.current.offsetWidth : 0;
+ availableWidth += rightScrollRef.current ? rightScrollRef.current.offsetWidth : 0;
+ }
+ setIsShowScrollBtn(tabCurrent.scrollWidth - 2 > availableWidth);
+ }, [context.luckysheetfile, context.allowEdit, context.luckysheetTableContentHW[0]]);
var onAddSheetClick = React.useCallback(function () {
return setTimeout(function () {
setContext(function (draftCtx) {
@@ -10704,7 +10715,8 @@ var SheetListItem = function SheetListItem(_ref) {
draftCtx.scrollLeft = 0;
draftCtx.scrollTop = 0;
draftCtx.luckysheet_select_status = false;
+3 -3
View File
@@ -13,7 +13,7 @@ patchedDependencies:
'@earendil-works/pi-ai@0.85.1': b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f
'@electron/osx-sign@1.3.3': 280e7b2816caf01423097d24e79107a66afeb563a348630ce9928ae8499a5861
'@fortune-sheet/core@1.0.4': 7d369e6feca3c674aeb089624e38e0a79ad61e695d6bdae89711876cbd178f24
'@fortune-sheet/react@1.0.4': c17549e117c8fb99f408497c1e84292c21f25516fb762a32518f91b4d9ab0f04
'@fortune-sheet/react@1.0.4': 2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37
'@yao-pkg/pkg@6.21.0': 75891fc22b277c145cafeab83e41baf4a70f5b8fe6dc0801c6a0b922d7ee88c7
exceljs@4.4.0: c1bf8ac8f319dab382f08e3a669b715e691d46c703ac5a4163eb383fbedd1d48
node-pty@1.2.0-beta.15: b40ae545608897914bd25fb009c97eeac478c34e8a910298ddcb01b746534bb0
@@ -5042,7 +5042,7 @@ importers:
version: 1.0.4(patch_hash=7d369e6feca3c674aeb089624e38e0a79ad61e695d6bdae89711876cbd178f24)
'@fortune-sheet/react':
specifier: 1.0.4
version: 1.0.4(patch_hash=c17549e117c8fb99f408497c1e84292c21f25516fb762a32518f91b4d9ab0f04)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 1.0.4(patch_hash=2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@testing-library/react':
specifier: ^16.1.0
version: 16.3.2(@testing-library/dom@10.4.2)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -22492,7 +22492,7 @@ snapshots:
'@formulajs/formulajs': 2.9.3
tiny-emitter: 2.1.0
'@fortune-sheet/react@1.0.4(patch_hash=c17549e117c8fb99f408497c1e84292c21f25516fb762a32518f91b4d9ab0f04)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@fortune-sheet/react@1.0.4(patch_hash=2932e52e79afcd4ced08cfa123b5eded786c85043da32f622f3e80af6fc96c37)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@fortune-sheet/core': 1.0.4(patch_hash=7d369e6feca3c674aeb089624e38e0a79ad61e695d6bdae89711876cbd178f24)
'@types/regenerator-runtime': 0.13.8