mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 05:20:54 +08:00
fix(web): keep extension tabs readable on narrow screens
This commit is contained in:
1 parent
f773b6e1fc
commit
77adffb160
3 files changed
+51
-8
No files matched your search
@@ -3672,8 +3672,8 @@ section[aria-label="切换书源"] button { margin:.35rem .4rem .35rem 0; paddin
|
||||
/* Generic declarative extension pages, scoped to the selected source instance. */
|
||||
.extension-screen .sources-body { width: 100%; max-width: 64rem; margin-inline: auto; box-sizing: border-box; }
|
||||
.extension-description { color: var(--muted, #777); margin: 0 0 16px; line-height: 1.6; }
|
||||
.extension-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border, #ddd); margin-bottom: 20px; }
|
||||
.extension-tabs button { flex: 1; padding: 12px 8px; background: transparent; color: inherit; border: 0; border-bottom: 3px solid transparent; cursor: pointer; font: inherit; }
|
||||
.extension-tabs { display: flex; flex-wrap: nowrap; gap: 4px; min-width: 0; max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; border-bottom: 1px solid var(--border, #ddd); margin-bottom: 20px; }
|
||||
.extension-tabs button { flex: 0 0 auto; white-space: nowrap; padding: 12px 16px; background: transparent; color: inherit; border: 0; border-bottom: 3px solid transparent; cursor: pointer; font: inherit; }
|
||||
.extension-tabs button[aria-selected="true"] { border-bottom-color: currentColor; font-weight: 700; }
|
||||
.extension-tabs button:focus-visible { outline: 2px solid currentColor; outline-offset: -3px; }
|
||||
.extension-screen h2 { font-size: 1rem; margin: 20px 0 12px; }
|
||||
@@ -3839,7 +3839,8 @@ section[aria-label="切换书源"] button { margin:.35rem .4rem .35rem 0; paddin
|
||||
.shelf-settings .switch-field { grid-template-columns: minmax(0, 1fr) auto; }
|
||||
.sources-header { width: calc(100% - 3rem); max-width: var(--collection-measure); margin-inline: auto; padding: 2rem .9rem .75rem; border-bottom: 0; }
|
||||
.sources-header h1 { font-size: 1.5rem; font-weight: 650; }
|
||||
.sources-hub .sources-tabs [role=tab], .extension-tabs button { flex: 0 1 auto; min-width: 9rem; padding-inline: 1.5rem; }
|
||||
.sources-hub .sources-tabs [role=tab] { flex: 0 1 auto; min-width: 9rem; padding-inline: 1.5rem; }
|
||||
.extension-tabs button { min-width: 7rem; padding-inline: 1.5rem; }
|
||||
.sources-hub .sources-body, .extension-screen .sources-body, .sources-hub .sources-tabs, .extension-toolbar { width: calc(100% - 3rem); max-width: var(--collection-measure); margin-inline: auto; padding-inline: .9rem; }
|
||||
.sources-body .sources-card { max-width: none; }
|
||||
.reader-screen { --reader-footer-height: 5.5rem; }
|
||||
|
||||
@@ -46,7 +46,7 @@ export class PluginPageScreen {
|
||||
if (error instanceof ApiError && error.isAuthFailure) this.options.onSignedOut();
|
||||
else this.error = error instanceof Error ? error.message : '操作失败';
|
||||
} finally {
|
||||
this.busy = false; this.draw();
|
||||
this.busy = false; this.draw(); this.revealActiveTab();
|
||||
if (previousTab !== this.activeTab) { const body = this.element.querySelector('.sources-body'); if (body) body.scrollTop = 0; }
|
||||
if (action) (this.element.querySelector<HTMLElement>('[role=tabpanel]') ?? this.element.querySelector<HTMLElement>('.sources-body'))?.focus({ preventScroll: true });
|
||||
}
|
||||
@@ -96,8 +96,17 @@ export class PluginPageScreen {
|
||||
</details>)}
|
||||
</>;
|
||||
}
|
||||
private revealActiveTab() {
|
||||
const list = this.element.querySelector<HTMLElement>('.extension-tabs');
|
||||
const selected = list?.querySelector<HTMLElement>('[aria-selected="true"]');
|
||||
if (!list || !selected) return;
|
||||
const viewport = list.getBoundingClientRect(), item = selected.getBoundingClientRect();
|
||||
// Only move the navigation strip; scrolling ancestors would move the page.
|
||||
if (item.left < viewport.left) list.scrollLeft += item.left - viewport.left;
|
||||
else if (item.right > viewport.right) list.scrollLeft += item.right - viewport.right;
|
||||
}
|
||||
private selectTab(id: string) {
|
||||
this.activeTab = id; this.error = ''; this.confirmation = undefined; this.draw();
|
||||
this.activeTab = id; this.error = ''; this.confirmation = undefined; this.draw(); this.revealActiveTab();
|
||||
const body = this.element.querySelector('.sources-body'); if (body) body.scrollTop = 0;
|
||||
}
|
||||
private view() {
|
||||
@@ -118,7 +127,7 @@ export class PluginPageScreen {
|
||||
else if (event.key === 'End') next = tabs.length - 1;
|
||||
else return;
|
||||
event.preventDefault(); this.selectTab(tabs[next]!.id);
|
||||
document.getElementById('extension-tab-' + this.activeTab)?.focus();
|
||||
document.getElementById('extension-tab-' + this.activeTab)?.focus({ preventScroll: true });
|
||||
}}>{tab.title}</button>)}</div>}
|
||||
</div>
|
||||
{status && <FloatingNotice message={status} busy={this.busy} error={isError} />}
|
||||
|
||||
@@ -141,9 +141,11 @@ try {
|
||||
// Deterministic extension responses exercise the shared page renderer's
|
||||
// busy/error/success feedback without depending on a third-party service.
|
||||
let releaseAction, failAction = true;
|
||||
const extensionPage = { title: '扩展配置', forms: [], tabs: [{ id: 'settings', title: '设置', forms: [
|
||||
const extensionPage = { title: '扩展配置', activeTab: 'browser', forms: [], tabs: [{ id: 'settings', title: '来源设置', forms: [
|
||||
{ id: 'save', title: '来源设置', submit: '保存配置', fields: [{ key: 'name', label: '配置名称', type: 'text', value: '示例' }] },
|
||||
], sections: [{ title: '配置项目', items: Array.from({ length: 12 }, (_, index) => ({ title: '项目 ' + index, description: '用于验证操作反馈不会移动内容。' })) }] }] };
|
||||
], sections: [{ title: '配置项目', items: Array.from({ length: 12 }, (_, index) => ({ title: '项目 ' + index, description: '用于验证操作反馈不会移动内容。' })) }] },
|
||||
...['订阅管理', '导入配置', '来源调试', '浏览器'].map((title, index) => ({ id: index === 3 ? 'browser' : 'tab-' + index, title, forms: [], description: title + '的配置内容' })),
|
||||
] };
|
||||
await page.route('**/api/v1/sources/feedback-fixture/pages/settings', async route => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await new Promise(resolve => { releaseAction = resolve; });
|
||||
@@ -152,9 +154,40 @@ try {
|
||||
}
|
||||
return route.fulfill({ json: extensionPage });
|
||||
});
|
||||
for (const width of [320, 390, 1280]) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.goto(base + '/#/sources/feedback-fixture/settings');
|
||||
await tab('浏览器').waitFor(); await idle();
|
||||
const assertActiveVisible = async () => {
|
||||
const bounds = await page.locator('.extension-tabs').evaluate(list => {
|
||||
const selected = list.querySelector('[aria-selected=true]').getBoundingClientRect();
|
||||
const viewport = list.getBoundingClientRect();
|
||||
return { left: selected.left, right: selected.right, start: viewport.left, end: viewport.right };
|
||||
});
|
||||
assert.ok(bounds.left >= bounds.start - 1 && bounds.right <= bounds.end + 1, 'selected tab is visible');
|
||||
assert.equal(await page.evaluate(() => window.scrollY), 0);
|
||||
};
|
||||
await assertActiveVisible();
|
||||
await tab('浏览器').focus(); await page.keyboard.press('Home');
|
||||
assert.equal(await tab('来源设置').getAttribute('aria-selected'), 'true');
|
||||
await assertActiveVisible();
|
||||
await shot('extension-tabs-first-' + width);
|
||||
await page.keyboard.press('End');
|
||||
assert.equal(await tab('浏览器').getAttribute('aria-selected'), 'true');
|
||||
await assertActiveVisible();
|
||||
const layout = await page.locator('.extension-tabs').evaluate(list => ({
|
||||
overflow: list.scrollWidth > list.clientWidth,
|
||||
heights: [...list.children].map(item => item.getBoundingClientRect().height),
|
||||
}));
|
||||
assert.equal(layout.overflow, width < 600);
|
||||
assert.ok(layout.heights.every(height => height >= 44 && height < 60), 'tabs stay single-line with touch-sized targets');
|
||||
await shot('extension-tabs-last-' + width);
|
||||
}
|
||||
delete extensionPage.activeTab;
|
||||
for (const width of [390, 1920]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(base + '/#/sources/feedback-fixture/settings');
|
||||
await tab('来源设置').click();
|
||||
await button('保存配置').waitFor(); await idle();
|
||||
const bodyBounds = await page.locator('.sources-body').boundingBox();
|
||||
const toolbarBounds = await page.locator('.extension-toolbar').boundingBox();
|
||||
|
||||
Reference in new issue
Block a user