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,
|
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) {
|
export function DockTab({ api }: IDockviewPanelHeaderProps) {
|
||||||
const subscribe = useCallback(
|
const subscribe = useCallback(
|
||||||
(onChange: () => void) => {
|
(onChange: () => void) => {
|
||||||
const subscriptions = [api.onDidTitleChange(onChange), api.onDidVisibilityChange(onChange)];
|
const subscription = api.onDidTitleChange(onChange);
|
||||||
return () => {
|
return () => subscription.dispose();
|
||||||
for (const subscription of subscriptions) subscription.dispose();
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
[api],
|
[api],
|
||||||
);
|
);
|
||||||
const title = useSyncExternalStore(subscribe, () => api.title ?? "");
|
const title = useSyncExternalStore(subscribe, () => api.title ?? "");
|
||||||
const shown = useSyncExternalStore(subscribe, () => api.isVisible);
|
const TypeIcon = isPanelId(api.id) ? TAB_ICONS[api.id] : null;
|
||||||
const TypeIcon = shown && isPanelId(api.id) ? TAB_ICONS[api.id] : null;
|
|
||||||
return (
|
return (
|
||||||
<div className="hf-dock-tab">
|
<div className="hf-dock-tab">
|
||||||
{TypeIcon ? <TypeIcon className="hf-dock-tab-icon" size={14} aria-hidden /> : null}
|
{TypeIcon ? <TypeIcon className="hf-dock-tab-icon" size={14} aria-hidden /> : null}
|
||||||
<span className="hf-dock-tab-label">{title}</span>
|
<span className="hf-dock-tab-label">{title}</span>
|
||||||
{shown ? (
|
{/* Same shape as dockview's own close control: a tab cannot hold a focusable button. */}
|
||||||
// Same shape as dockview's own close control: a tab cannot hold a focusable button.
|
<div
|
||||||
<div
|
role="button"
|
||||||
role="button"
|
tabIndex={-1}
|
||||||
tabIndex={-1}
|
aria-label={`Close ${title}`}
|
||||||
aria-label={`Close ${title}`}
|
className="hf-dock-tab-close"
|
||||||
className="hf-dock-tab-close"
|
onPointerDown={(event) => event.stopPropagation()}
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
onClick={(event) => {
|
||||||
onClick={(event) => {
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
api.close();
|
||||||
api.close();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<X size={12} aria-hidden />
|
||||||
<X size={12} aria-hidden />
|
</div>
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// @vitest-environment happy-dom
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import { createRoot, type Root } from "react-dom/client";
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
@@ -40,6 +42,13 @@ const original = {
|
|||||||
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
offsetWidth: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetWidth"),
|
||||||
};
|
};
|
||||||
beforeAll(() => {
|
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, {
|
Object.defineProperties(HTMLElement.prototype, {
|
||||||
offsetLeft: { ...layout.offsetLeft, configurable: true },
|
offsetLeft: { ...layout.offsetLeft, configurable: true },
|
||||||
offsetWidth: { ...layout.offsetWidth, 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 tab = (id: PanelId) => host.querySelector<HTMLElement>(`[data-tab-panel-id="${id}"]`);
|
||||||
const stripOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-tabs-container");
|
const stripOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-tabs-container");
|
||||||
const groupOf = (id: PanelId) => tab(id)?.closest<HTMLElement>(".dv-groupview");
|
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) {
|
async function activate(id: PanelId) {
|
||||||
// dockview settles the active panel on a microtask; `act(async)` waits for it.
|
// 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", () => {
|
describe("dock tabs", () => {
|
||||||
it("draw the type icon and close glyph on the shown tab only", () => {
|
it("draw the type icon and close glyph on the shown tab only", () => {
|
||||||
for (const id of ["design", "compositions"] as const) {
|
for (const id of ["design", "compositions"] as const) {
|
||||||
expect(tab(id)?.querySelector(".hf-dock-tab-icon")).not.toBeNull();
|
expect(shows(id, "icon")).toBe(true);
|
||||||
expect(tab(id)?.querySelector(".hf-dock-tab-close")).not.toBeNull();
|
expect(shows(id, "close")).toBe(true);
|
||||||
}
|
}
|
||||||
for (const id of ["layers", "renders", "variables", "assets", "code", "catalog"] as const) {
|
for (const id of ["layers", "renders", "variables", "assets", "code", "catalog"] as const) {
|
||||||
expect(tab(id)?.querySelector(".hf-dock-tab-icon")).toBeNull();
|
expect(shows(id, "icon")).toBe(false);
|
||||||
expect(tab(id)?.querySelector(".hf-dock-tab-close")).toBeNull();
|
expect(shows(id, "close")).toBe(false);
|
||||||
expect(tab(id)?.textContent).toBe(tab(id)?.getAttribute("aria-label"));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("move the icon to the newly shown tab and drop it from the old one", async () => {
|
it("move the icon to the newly shown tab and drop it from the old one", async () => {
|
||||||
await activate("layers");
|
await activate("layers");
|
||||||
expect(tab("layers")?.querySelector(".hf-dock-tab-icon")).not.toBeNull();
|
expect(shows("layers", "icon")).toBe(true);
|
||||||
expect(tab("design")?.querySelector(".hf-dock-tab-icon")).toBeNull();
|
expect(shows("design", "icon")).toBe(false);
|
||||||
expect(tab("design")?.querySelector(".hf-dock-tab-close")).toBeNull();
|
expect(shows("design", "close")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("name the close control after the panel, and close only that panel", async () => {
|
it("name the close control after the panel, and close only that panel", async () => {
|
||||||
|
|||||||
@@ -136,6 +136,11 @@
|
|||||||
gap: 6px;
|
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 {
|
.hf-dock-tab-icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
animation: hf-dock-tab-icon-in 80ms ease-out;
|
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 {
|
export function installTabFill(api: DockviewApi, root: HTMLElement): () => void {
|
||||||
const lists = () => root.querySelectorAll<HTMLElement>(".dv-tabs-container");
|
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 resizeObserver = new ResizeObserver(() => placeAll());
|
||||||
const observed = new Set<Element>();
|
const observed = new Set<Element>();
|
||||||
function observe(elements: Iterable<Element>) {
|
function observe(elements: Iterable<Element>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user