mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): swap the tab icon in the same frame as the tab
This commit is contained in:
@@ -31,40 +31,38 @@ const TAB_ICONS: Record<PanelId, Icon> = {
|
||||
slideshow: Presentation,
|
||||
};
|
||||
|
||||
/** A dock tab. Only the shown tab carries its type icon and close glyph; the rest are labels. */
|
||||
/**
|
||||
* A dock tab. dock.css shows the type icon and close glyph on the shown tab only, keyed on
|
||||
* dockview's own tab class, so they swap in the frame the tab changes rather than a render later.
|
||||
*/
|
||||
export function DockTab({ api }: IDockviewPanelHeaderProps) {
|
||||
const subscribe = useCallback(
|
||||
(onChange: () => void) => {
|
||||
const subscriptions = [api.onDidTitleChange(onChange), api.onDidVisibilityChange(onChange)];
|
||||
return () => {
|
||||
for (const subscription of subscriptions) subscription.dispose();
|
||||
};
|
||||
const subscription = api.onDidTitleChange(onChange);
|
||||
return () => subscription.dispose();
|
||||
},
|
||||
[api],
|
||||
);
|
||||
const title = useSyncExternalStore(subscribe, () => api.title ?? "");
|
||||
const shown = useSyncExternalStore(subscribe, () => api.isVisible);
|
||||
const TypeIcon = shown && isPanelId(api.id) ? TAB_ICONS[api.id] : null;
|
||||
const TypeIcon = isPanelId(api.id) ? TAB_ICONS[api.id] : null;
|
||||
return (
|
||||
<div className="hf-dock-tab">
|
||||
{TypeIcon ? <TypeIcon className="hf-dock-tab-icon" size={14} aria-hidden /> : null}
|
||||
<span className="hf-dock-tab-label">{title}</span>
|
||||
{shown ? (
|
||||
// Same shape as dockview's own close control: a tab cannot hold a focusable button.
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={`Close ${title}`}
|
||||
className="hf-dock-tab-close"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
api.close();
|
||||
}}
|
||||
>
|
||||
<X size={12} aria-hidden />
|
||||
</div>
|
||||
) : null}
|
||||
{/* Same shape as dockview's own close control: a tab cannot hold a focusable button. */}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label={`Close ${title}`}
|
||||
className="hf-dock-tab-close"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
api.close();
|
||||
}}
|
||||
>
|
||||
<X size={12} aria-hidden />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -40,6 +42,13 @@ const original = {
|
||||
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
||||
};
|
||||
beforeAll(() => {
|
||||
// The shown-tab-only rule lives in dock.css; apply it so computed display is what a user sees.
|
||||
const style = document.createElement("style");
|
||||
style.textContent = readFileSync(path.join(import.meta.dirname, "dock.css"), "utf8").replace(
|
||||
/@import[^;]*;/,
|
||||
"",
|
||||
);
|
||||
document.head.append(style);
|
||||
Object.defineProperties(HTMLElement.prototype, {
|
||||
offsetLeft: { ...layout.offsetLeft, configurable: true },
|
||||
offsetWidth: { ...layout.offsetWidth, configurable: true },
|
||||
@@ -83,6 +92,10 @@ afterEach(() => {
|
||||
const tab = (id: PanelId) => host.querySelector<HTMLElement>(`[data-tab-panel-id="${id}"]`);
|
||||
const stripOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-tabs-container");
|
||||
const groupOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-groupview");
|
||||
const shows = (id: PanelId, part: "icon" | "close") => {
|
||||
const element = tab(id)?.querySelector(`.hf-dock-tab-${part}`);
|
||||
return element ? getComputedStyle(element).display !== "none" : false;
|
||||
};
|
||||
|
||||
async function activate(id: PanelId) {
|
||||
// dockview settles the active panel on a microtask; `act(async)` waits for it.
|
||||
@@ -92,21 +105,20 @@ async function activate(id: PanelId) {
|
||||
describe("dock tabs", () => {
|
||||
it("draw the type icon and close glyph on the shown tab only", () => {
|
||||
for (const id of ["design", "compositions"] as const) {
|
||||
expect(tab(id)?.querySelector(".hf-dock-tab-icon")).not.toBeNull();
|
||||
expect(tab(id)?.querySelector(".hf-dock-tab-close")).not.toBeNull();
|
||||
expect(shows(id, "icon")).toBe(true);
|
||||
expect(shows(id, "close")).toBe(true);
|
||||
}
|
||||
for (const id of ["layers", "renders", "variables", "assets", "code", "catalog"] as const) {
|
||||
expect(tab(id)?.querySelector(".hf-dock-tab-icon")).toBeNull();
|
||||
expect(tab(id)?.querySelector(".hf-dock-tab-close")).toBeNull();
|
||||
expect(tab(id)?.textContent).toBe(tab(id)?.getAttribute("aria-label"));
|
||||
expect(shows(id, "icon")).toBe(false);
|
||||
expect(shows(id, "close")).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("move the icon to the newly shown tab and drop it from the old one", async () => {
|
||||
await activate("layers");
|
||||
expect(tab("layers")?.querySelector(".hf-dock-tab-icon")).not.toBeNull();
|
||||
expect(tab("design")?.querySelector(".hf-dock-tab-icon")).toBeNull();
|
||||
expect(tab("design")?.querySelector(".hf-dock-tab-close")).toBeNull();
|
||||
expect(shows("layers", "icon")).toBe(true);
|
||||
expect(shows("design", "icon")).toBe(false);
|
||||
expect(shows("design", "close")).toBe(false);
|
||||
});
|
||||
|
||||
it("name the close control after the panel, and close only that panel", async () => {
|
||||
|
||||
@@ -136,6 +136,11 @@
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hf-dock .dv-inactive-tab .hf-dock-tab-icon,
|
||||
.hf-dock .dv-inactive-tab .hf-dock-tab-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hf-dock-tab-icon {
|
||||
flex: none;
|
||||
animation: hf-dock-tab-icon-in 80ms ease-out;
|
||||
|
||||
@@ -44,7 +44,7 @@ function markClippedEdges(list: HTMLElement) {
|
||||
*/
|
||||
export function installTabFill(api: DockviewApi, root: HTMLElement): () => void {
|
||||
const lists = () => root.querySelectorAll<HTMLElement>(".dv-tabs-container");
|
||||
// Tab widths change when a tab gains or loses its icon, after React renders it.
|
||||
// Tab widths also change with no dockview event: a renamed title, a late web font.
|
||||
const resizeObserver = new ResizeObserver(() => placeAll());
|
||||
const observed = new Set<Element>();
|
||||
function observe(elements: Iterable<Element>) {
|
||||
|
||||
Reference in New Issue
Block a user