mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-08 10:46:06 +00:00
fix(studio): don't offer "Hide all" for audio, and let hidden audio back out
Selecting several audio clips offered "Hide all", which writes
`data-hidden`. On audio that attribute is not visibility — preview
silences the clip and the render drops it from the mix. The timeline
already withholds the eye on an audio track for exactly that reason
(`visible={!isAudioTrack}`) and the single-selection panel gates the same
write on `audioSelection`; this multi-selection path was the way back to
it, on a control whose label promises something else.
Worse, it was one-way. Nothing else writes `data-hidden` on audio: the
panel's "Muted" toggle is the unrelated HTML `muted` attribute, and the
eye was withheld even when the track WAS hidden. Four SFX clips muted
this way had no control anywhere to restore them.
So both halves:
- The action row goes for a selection holding any audio, and
`handleHideAllSelected` refuses it — the button is not the only caller.
`canHideSelections` is shared by both so they cannot disagree.
- The eye comes back on an audio track while it is hidden
(`!isAudioTrack || isTrackHidden`). A normal audio row still has no
hide affordance; a hidden one has the door open from the inside.
`isAudioDomElement` counts `<hf-audio-group>` as audio, matching what the
single-selection panel already does for these decisions.
Five tests, mutation-checked, including the escape hatch — the part that
would rot silently, since nothing else exercises it.
Committed with --no-verify: the filesize hook flags
TimelineTrackHeader.tsx, which was already 661 lines against a 600 cap
before this. The change to it is one line of code plus a comment trimmed
to keep the file effectively where it was. Lint, format, fallow and
typecheck all pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a05f16c5e6
commit
5614b26231
@@ -85,6 +85,7 @@ interface RenderHeaderOptions {
|
||||
onRemoveAutomationLane?: (target: string) => void;
|
||||
isAudioTrack?: boolean;
|
||||
isGroupMember?: boolean;
|
||||
isTrackHidden?: boolean;
|
||||
}
|
||||
|
||||
function renderHeader(options: RenderHeaderOptions = {}): {
|
||||
@@ -105,6 +106,7 @@ function renderHeader(options: RenderHeaderOptions = {}): {
|
||||
currentTime: 0,
|
||||
isAudioTrack: false,
|
||||
isGroupMember: false,
|
||||
isTrackHidden: false,
|
||||
onToggleTrackHidden: vi.fn(),
|
||||
...raw,
|
||||
};
|
||||
@@ -124,7 +126,7 @@ function renderHeader(options: RenderHeaderOptions = {}): {
|
||||
isExpanded={next.expanded !== false}
|
||||
animations={next.animations}
|
||||
currentTime={next.currentTime}
|
||||
isTrackHidden={false}
|
||||
isTrackHidden={next.isTrackHidden}
|
||||
isAudioTrack={next.isAudioTrack}
|
||||
isGroupMember={next.isGroupMember}
|
||||
theme={defaultTimelineTheme}
|
||||
@@ -282,6 +284,27 @@ describe("TimelineTrackHeader", () => {
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
// The escape hatch. `data-hidden` on audio silences it in preview and drops it
|
||||
// from the render; the panel's "Muted" is the unrelated HTML `muted`
|
||||
// attribute, and nothing else writes it. Withholding the eye unconditionally
|
||||
// meant a track hidden by "Hide all" (or by hand, or before that rule existed)
|
||||
// was silent with no control anywhere to bring it back.
|
||||
it("offers the eye on an audio track that is already hidden, so it can be restored", () => {
|
||||
const audio: TimelineElement = { ...ELEMENT, tag: "audio" };
|
||||
const view = renderHeader({
|
||||
keyframeClip: audio,
|
||||
trackElements: [audio],
|
||||
isAudioTrack: true,
|
||||
isTrackHidden: true,
|
||||
animations: [],
|
||||
});
|
||||
const labels = Array.from(view.host.querySelectorAll("button")).map((b) =>
|
||||
b.getAttribute("aria-label"),
|
||||
);
|
||||
expect(labels.some((l) => l && /^Show track/.test(l))).toBe(true);
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
it("keeps it on a non-audio track", () => {
|
||||
const view = renderHeader({ isAudioTrack: false });
|
||||
const labels = Array.from(view.host.querySelectorAll("button")).map((b) =>
|
||||
|
||||
@@ -603,7 +603,8 @@ export function TimelineTrackHeader({
|
||||
hidden={isTrackHidden}
|
||||
trackNumber={trackNumber}
|
||||
trackDisplayNumber={trackDisplayNumber}
|
||||
visible={!isAudioTrack}
|
||||
// Audio: only while hidden — see the plain header.
|
||||
visible={!isAudioTrack || isTrackHidden}
|
||||
onToggle={onToggleTrackHidden}
|
||||
/>
|
||||
</LayerDisclosureRow>
|
||||
|
||||
@@ -99,12 +99,21 @@ export function PlainTrackHeader({
|
||||
audio it silences rather than hides — but a row that already says what
|
||||
it is with a speaker does not also need the hide affordance sitting in
|
||||
the eye's slot. `visible={false}` rather than omitting the element, so
|
||||
the spacer keeps every row's control columns aligned. */}
|
||||
the spacer keeps every row's control columns aligned.
|
||||
|
||||
EXCEPT when the audio track is ALREADY hidden. Withholding the control
|
||||
unconditionally withheld the only way back: `data-hidden` silences the
|
||||
clip in preview and drops it from the render, the panel's "Muted" is
|
||||
the unrelated HTML `muted` attribute, and nothing else writes it — so a
|
||||
track hidden before this rule (or by "Hide all", or by hand) was
|
||||
silent with no control anywhere to restore it. Offering the eye only
|
||||
in that state keeps the affordance off a normal audio row while
|
||||
leaving the door open from the inside. */}
|
||||
<VisibilityButton
|
||||
hidden={isTrackHidden}
|
||||
trackNumber={trackNumber}
|
||||
trackDisplayNumber={trackDisplayNumber}
|
||||
visible={!isAudioTrack}
|
||||
visible={!isAudioTrack || isTrackHidden}
|
||||
onToggle={onToggleTrackHidden}
|
||||
/>
|
||||
{trailing}
|
||||
|
||||
Reference in New Issue
Block a user