mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
fix(m3u): keep the sidebar mounted while the guide is open
Guide mode wrapped the sidebar in `@if (!guideOpen())`, so opening the guide destroyed `app-channel-list-container`, whose `ngOnDestroy` dispatches `resetActiveChannel()`. That cleared the active channel, which unmounted the block hosting `app-epg-guide` and tripped the `!canOpenGuide()` effect into closing the guide again: the guide never appeared and the page dropped to "Please select a channel". The sidebar now stays mounted and is hidden with `.sidebar--guide-hidden` plus `inert`, so it is neither focusable nor read by assistive technology while the guide owns the layout. Hiding also preserves the channel list's scroll position across guide toggles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8d73286d93
commit
1fc8e1cf73
3 files changed
+90
-58
No files matched your search
@@ -1,55 +1,62 @@
|
||||
@if (!guideOpen()) {
|
||||
<div
|
||||
class="sidebar"
|
||||
[class.sidebar-collapsed]="isSidebarCollapsed()"
|
||||
appResizable
|
||||
[minWidth]="sidebarMinWidth"
|
||||
[maxWidth]="sidebarMaxWidth"
|
||||
[defaultWidth]="sidebarWidth()"
|
||||
[style.width.px]="sidebarWidth()"
|
||||
[storageKey]="sidebarStorageKey()"
|
||||
(widthChange)="onSidebarWidthChange($event)"
|
||||
(resizeEnd)="onSidebarResizeEnd($event)"
|
||||
>
|
||||
@if (activeView() !== 'groups' && activeView() !== 'all') {
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
class="sidebar-collapse-toggle"
|
||||
[attr.aria-label]="'LAYOUT.HIDE_CHANNELS_LIST' | translate"
|
||||
[matTooltip]="'LAYOUT.TOGGLE_SIDEBAR_TOOLTIP' | translate"
|
||||
[attr.aria-pressed]="isSidebarCollapsed()"
|
||||
(click)="toggleSidebar()"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@defer {
|
||||
<app-sidebar
|
||||
[channels]="(channels$ | async)!"
|
||||
[channelsLoading]="channelsLoading()"
|
||||
[showPlaylistHeader]="!isWorkspaceLayout"
|
||||
[activeView]="activeView()"
|
||||
[sidebarWidth]="sidebarWidth()"
|
||||
(sidebarWidthRequested)="onGroupedSidebarWidthRequested($event)"
|
||||
(sidebarWidthRequestEnded)="
|
||||
onGroupedSidebarWidthRequestEnded($event)
|
||||
"
|
||||
(sidebarToggleRequested)="toggleSidebar()"
|
||||
/>
|
||||
} @loading {
|
||||
<app-channel-list-loading-state
|
||||
[view]="activeView()"
|
||||
[showEpg]="supportsEpg"
|
||||
/>
|
||||
} @placeholder {
|
||||
<app-channel-list-loading-state
|
||||
[view]="activeView()"
|
||||
[showEpg]="supportsEpg"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!--
|
||||
Guide mode HIDES this sidebar instead of removing it: destroying it would
|
||||
destroy `app-channel-list-container`, whose `ngOnDestroy` dispatches
|
||||
`resetActiveChannel()` — that clears the active channel the guide itself
|
||||
needs, so the guide would close again the moment it opened. `inert` keeps
|
||||
the hidden rail out of the tab order and away from assistive technology.
|
||||
-->
|
||||
<div
|
||||
class="sidebar"
|
||||
[class.sidebar-collapsed]="isSidebarCollapsed()"
|
||||
[class.sidebar--guide-hidden]="guideOpen()"
|
||||
[attr.inert]="guideOpen() ? '' : null"
|
||||
appResizable
|
||||
[minWidth]="sidebarMinWidth"
|
||||
[maxWidth]="sidebarMaxWidth"
|
||||
[defaultWidth]="sidebarWidth()"
|
||||
[style.width.px]="sidebarWidth()"
|
||||
[storageKey]="sidebarStorageKey()"
|
||||
(widthChange)="onSidebarWidthChange($event)"
|
||||
(resizeEnd)="onSidebarResizeEnd($event)"
|
||||
>
|
||||
@if (activeView() !== 'groups' && activeView() !== 'all') {
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
class="sidebar-collapse-toggle"
|
||||
[attr.aria-label]="'LAYOUT.HIDE_CHANNELS_LIST' | translate"
|
||||
[matTooltip]="'LAYOUT.TOGGLE_SIDEBAR_TOOLTIP' | translate"
|
||||
[attr.aria-pressed]="isSidebarCollapsed()"
|
||||
(click)="toggleSidebar()"
|
||||
>
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@defer {
|
||||
<app-sidebar
|
||||
[channels]="(channels$ | async)!"
|
||||
[channelsLoading]="channelsLoading()"
|
||||
[showPlaylistHeader]="!isWorkspaceLayout"
|
||||
[activeView]="activeView()"
|
||||
[sidebarWidth]="sidebarWidth()"
|
||||
(sidebarWidthRequested)="onGroupedSidebarWidthRequested($event)"
|
||||
(sidebarWidthRequestEnded)="
|
||||
onGroupedSidebarWidthRequestEnded($event)
|
||||
"
|
||||
(sidebarToggleRequested)="toggleSidebar()"
|
||||
/>
|
||||
} @loading {
|
||||
<app-channel-list-loading-state
|
||||
[view]="activeView()"
|
||||
[showEpg]="supportsEpg"
|
||||
/>
|
||||
} @placeholder {
|
||||
<app-channel-list-loading-state
|
||||
[view]="activeView()"
|
||||
[showEpg]="supportsEpg"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="content-container"
|
||||
|
||||
@@ -147,6 +147,17 @@
|
||||
}
|
||||
|
||||
// ─── Guide mode ─────────────────────────────────────────────────────────────
|
||||
// The sidebar stays MOUNTED while the guide is open and is only hidden: its
|
||||
// channel-list container dispatches `resetActiveChannel()` from `ngOnDestroy`,
|
||||
// so removing it would clear the active channel the guide needs and close the
|
||||
// guide again. `!important` beats the resizable directive's inline width, and
|
||||
// it is declared after the phone breakpoint so the bottom-drawer rules
|
||||
// (`height: 50vh`, borders) cannot leave a phantom band on small screens.
|
||||
// Hiding rather than removing also preserves the list's scroll position.
|
||||
.sidebar.sidebar--guide-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// The player keeps its DOM position and is only re-flowed into a docked strip;
|
||||
// no remount, so playback (and native-view Embedded MPV bounds) survive.
|
||||
.content-container.is-guide {
|
||||
|
||||
+20
-6
@@ -273,7 +273,7 @@ describe('VideoPlayerComponent', () => {
|
||||
expect(headerContext.action()).toBeNull();
|
||||
});
|
||||
|
||||
it('opens the guide in place of sidebar and timeline without remounting the player', () => {
|
||||
it('opens the guide, hides the sidebar without unmounting it, and keeps the player', () => {
|
||||
syncStoreState(sampleChannel);
|
||||
player.set(VideoPlayer.VideoJs);
|
||||
fixture.detectChanges();
|
||||
@@ -284,7 +284,9 @@ describe('VideoPlayerComponent', () => {
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('app-epg-timeline')
|
||||
).not.toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('.sidebar')).not.toBeNull();
|
||||
const sidebarBefore = fixture.nativeElement.querySelector('.sidebar');
|
||||
expect(sidebarBefore).not.toBeNull();
|
||||
storeMock.dispatch.mockClear();
|
||||
|
||||
component.openGuide();
|
||||
fixture.detectChanges();
|
||||
@@ -295,7 +297,18 @@ describe('VideoPlayerComponent', () => {
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('app-epg-guide-now-playing')
|
||||
).not.toBeNull();
|
||||
expect(fixture.nativeElement.querySelector('.sidebar')).toBeNull();
|
||||
// Regression: the sidebar used to sit behind `@if (!guideOpen())`, so
|
||||
// opening the guide destroyed the channel-list container, whose
|
||||
// `ngOnDestroy` dispatches `resetActiveChannel()`. That cleared the
|
||||
// active channel and closed the guide again the moment it opened.
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
ChannelActions.resetActiveChannel()
|
||||
);
|
||||
const sidebarWhileOpen =
|
||||
fixture.nativeElement.querySelector('.sidebar');
|
||||
expect(sidebarWhileOpen).toBe(sidebarBefore);
|
||||
expect(sidebarWhileOpen.classList).toContain('sidebar--guide-hidden');
|
||||
expect(sidebarWhileOpen.hasAttribute('inert')).toBe(true);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('app-epg-timeline')
|
||||
).toBeNull();
|
||||
@@ -310,9 +323,10 @@ describe('VideoPlayerComponent', () => {
|
||||
component.closeGuide();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('app-epg-guide')).toBeNull();
|
||||
// The sidebar list itself sits behind an `@defer`, so the assertion
|
||||
// targets the container the guide mode actually removes.
|
||||
expect(fixture.nativeElement.querySelector('.sidebar')).not.toBeNull();
|
||||
const sidebarAfter = fixture.nativeElement.querySelector('.sidebar');
|
||||
expect(sidebarAfter).toBe(sidebarBefore);
|
||||
expect(sidebarAfter.classList).not.toContain('sidebar--guide-hidden');
|
||||
expect(sidebarAfter.hasAttribute('inert')).toBe(false);
|
||||
expect(fixture.nativeElement.querySelector('app-web-player-view')).toBe(
|
||||
playerBefore
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user