51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
export interface TabWidth<T extends string> {
|
|
key: T;
|
|
width: number;
|
|
}
|
|
|
|
export interface TabRowInput<T extends string> {
|
|
tabs: TabWidth<T>[];
|
|
/** Width the row may occupy, padding already taken off. */
|
|
available: number;
|
|
gap: number;
|
|
/** Width of the button that opens the hidden ones; only claimed when something is hidden. */
|
|
overflowWidth: number;
|
|
active: T | null;
|
|
}
|
|
|
|
export interface TabRowLayout<T extends string> {
|
|
shown: T[];
|
|
hidden: T[];
|
|
}
|
|
|
|
function rowWidth<T extends string>(tabs: TabWidth<T>[], gap: number): number {
|
|
if (tabs.length === 0) return 0;
|
|
return tabs.reduce((sum, tab) => sum + tab.width, 0) + gap * (tabs.length - 1);
|
|
}
|
|
|
|
/**
|
|
* Which tabs fit in the row and which go behind the overflow button. Tabs keep their order; the
|
|
* active one is always shown, even if the row can hold nothing else.
|
|
*/
|
|
export function tabRowLayout<T extends string>(input: TabRowInput<T>): TabRowLayout<T> {
|
|
const { tabs, available, gap, overflowWidth, active } = input;
|
|
if (rowWidth(tabs, gap) <= available) return { shown: tabs.map((tab) => tab.key), hidden: [] };
|
|
|
|
const budget = available - overflowWidth - gap;
|
|
const shown: TabWidth<T>[] = [];
|
|
for (const tab of tabs) {
|
|
if (rowWidth([...shown, tab], gap) > budget) break;
|
|
shown.push(tab);
|
|
}
|
|
if (!shown.some((tab) => tab.key === active)) {
|
|
const activeTab = tabs.find((tab) => tab.key === active);
|
|
if (activeTab) {
|
|
// The prefix stops before the active tab, so appending it keeps the row in order.
|
|
while (shown.length > 0 && rowWidth([...shown, activeTab], gap) > budget) shown.pop();
|
|
shown.push(activeTab);
|
|
}
|
|
}
|
|
const kept = new Set(shown.map((tab) => tab.key));
|
|
return { shown: [...kept], hidden: tabs.map((tab) => tab.key).filter((key) => !kept.has(key)) };
|
|
}
|