diff --git a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md index 13b44c5e7..075413716 100644 --- a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md +++ b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md @@ -261,7 +261,7 @@ git commit -m "fix(ui): expose guide collapse capability" - Modify: `libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.{ts,html,scss,spec.ts}` -- [ ] **Step 1: Write failing tests for panel-specific outputs** +- [x] **Step 1: Write failing tests for panel-specific outputs** Assert that All Channels emits `channelsPanelExpandedChange(false)` from its header. Assert that Groups has separate Groups and Channels buttons/restore @@ -282,7 +282,7 @@ expect(document.activeElement).toBe(groupsRestore); The loading-state spec must prove both controls remain in the Groups skeleton and Channels remains in the All Channels skeleton. -- [ ] **Step 2: Run Components tests and verify RED** +- [x] **Step 2: Run Components tests and verify RED** ```bash pnpm nx test components --runInBand @@ -290,7 +290,7 @@ pnpm nx test components --runInBand Expected: FAIL because panel-specific inputs/outputs and controls do not exist. -- [ ] **Step 3: Implement the All Channels and loading contracts** +- [x] **Step 3: Implement the All Channels and loading contracts** Replace ambiguous `sidebarToggleRequested` with: @@ -303,7 +303,7 @@ Use `live-channels-panel-hide` and `live-channels-panel-restore` test IDs, `aria-controls="live-channels-panel"`, `aria-expanded`, translated names, and 40px targets. Loading-state controls emit the same explicit requested value. -- [ ] **Step 4: Implement independent Groups layout** +- [x] **Step 4: Implement independent Groups layout** Give `GroupsViewComponent`: @@ -320,13 +320,13 @@ the hidden width, and focus the counterpart control after a zero-delay render turn. The Groups toggle is the final action after search/sort/manage; the Channels toggle remains the final action after channel sort. -- [ ] **Step 5: Thread the contract through `ChannelListContainerComponent`** +- [x] **Step 5: Thread the contract through `ChannelListContainerComponent`** Add matching inputs and outputs and pass them through the All/Groups/loading branches. Do not render panel actions for legacy M3U `favorites`/`recent` branches. -- [ ] **Step 6: Run and verify GREEN** +- [x] **Step 6: Run and verify GREEN** ```bash pnpm nx test components --runInBand @@ -335,7 +335,7 @@ pnpm nx lint components Expected: both commands exit 0. -- [ ] **Step 7: Commit** +- [x] **Step 7: Commit** ```bash git add libs/ui/components diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html index accebb6d3..b60b159be 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.html @@ -15,9 +15,12 @@ type="button" mat-icon-button class="all-channels-sidebar-toggle" + data-testid="live-channels-panel-hide" + aria-controls="live-channels-panel" + [attr.aria-expanded]="channelsPanelExpanded()" [attr.aria-label]="'LAYOUT.HIDE_CHANNELS_LIST' | translate" [matTooltip]="'LAYOUT.TOGGLE_SIDEBAR_TOOLTIP' | translate" - (click)="sidebarToggleRequested.emit()" + (click)="channelsPanelExpandedChange.emit(false)" > chevron_left diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss index e74c10efb..08d171ac0 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.scss @@ -63,6 +63,8 @@ .all-channels-sidebar-toggle { flex: 0 0 auto; margin-right: -4px; + min-width: 40px; + min-height: 40px; } .all-channels-divider { diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts index 4ce8801ee..99a2eaa3f 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.spec.ts @@ -191,17 +191,23 @@ describe('AllChannelsViewComponent', () => { ).toEqual(['Zulu Vision', 'Middle News', 'Alpha Signal']); }); - it('emits sidebar toggle requests from the inline header action', () => { - const sidebarToggleRequested = jest.fn(); - component.sidebarToggleRequested.subscribe(sidebarToggleRequested); + it('requests an explicit Channels collapse from the inline header action', () => { + const channelsPanelExpandedChange = jest.fn(); + component.channelsPanelExpandedChange.subscribe( + channelsPanelExpandedChange + ); const toggleButton = fixture.nativeElement.querySelector( - '.all-channels-sidebar-toggle' + '[data-testid="live-channels-panel-hide"]' ) as HTMLButtonElement; + expect(toggleButton.getAttribute('aria-controls')).toBe( + 'live-channels-panel' + ); + expect(toggleButton.getAttribute('aria-expanded')).toBe('true'); toggleButton.click(); - expect(sidebarToggleRequested).toHaveBeenCalledTimes(1); + expect(channelsPanelExpandedChange).toHaveBeenCalledWith(false); }); it('stores viewport coordinates for the context menu and opens the dialog for that channel', async () => { diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts index 1516362ea..abb077155 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-view/all-channels-view.component.ts @@ -41,6 +41,9 @@ export type { ChannelEpgMetadata } from '../epg-enrichment.util'; templateUrl: './all-channels-view.component.html', styleUrls: ['./all-channels-view.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + host: { + id: 'live-channels-panel', + }, imports: [ ChannelListItemComponent, MatButtonModule, @@ -93,8 +96,8 @@ export class AllChannelsViewComponent { event: MouseEvent; }>(); - /** Emits when the user clicks the inline collapse toggle in the list header */ - readonly sidebarToggleRequested = output(); + readonly channelsPanelExpanded = input(true); + readonly channelsPanelExpandedChange = output(); readonly contextMenuChannel = signal(null); readonly contextMenuPosition = signal({ diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html index d6be05b29..5e64da173 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html @@ -2,6 +2,10 @@ } @else if (displayedChannels().length) { @if (viewTitle() && activeView() !== 'groups' && activeView() !== 'all') { @@ -36,10 +40,13 @@ [itemSize]="itemSize()" [activeChannelUrl]="activeChannelUrl()" [favoriteIds]="favoriteIds()" + [channelsPanelExpanded]="channelsPanelExpanded()" (channelSelected)="onChannelSelected($event)" (channelPlaybackRequested)="onChannelPlaybackRequested($event)" (favoriteToggled)="onFavoriteToggled($event)" - (sidebarToggleRequested)="sidebarToggleRequested.emit()" + (channelsPanelExpandedChange)=" + channelsPanelExpandedChange.emit($event) + " /> } @case ('groups') { @@ -55,13 +62,20 @@ [favoriteIds]="favoriteIds()" [hiddenGroupTitles]="hiddenGroupTitles()" [sidebarWidth]="sidebarWidth()" + [groupsPanelExpanded]="groupsPanelExpanded()" + [channelsPanelExpanded]="channelsPanelExpanded()" (channelSelected)="onChannelSelected($event)" (channelPlaybackRequested)="onChannelPlaybackRequested($event)" (favoriteToggled)="onFavoriteToggled($event)" (hiddenGroupTitlesChanged)="onHiddenGroupTitlesChanged($event)" (sidebarWidthRequested)="onSidebarWidthRequested($event)" (sidebarWidthRequestEnded)="onSidebarWidthRequestEnded($event)" - (sidebarToggleRequested)="sidebarToggleRequested.emit()" + (groupsPanelExpandedChange)=" + groupsPanelExpandedChange.emit($event) + " + (channelsPanelExpandedChange)=" + channelsPanelExpandedChange.emit($event) + " /> } @case ('favorites') { diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index dbaec930d..a3009df10 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -184,10 +184,13 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { readonly activeView = input('all'); readonly channelsLoading = input(false); readonly recentItems = input([]); + readonly groupsPanelExpanded = input(true); + readonly channelsPanelExpanded = input(true); readonly sidebarWidth = input(null); readonly sidebarWidthRequested = output(); readonly sidebarWidthRequestEnded = output(); - readonly sidebarToggleRequested = output(); + readonly groupsPanelExpandedChange = output(); + readonly channelsPanelExpandedChange = output(); readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route); private readonly routeSearchTerm = queryParamSignal( this.route, @@ -442,8 +445,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { } private getPlaylistEpgLookupOptions(): - | { sourceUrls: string[] } - | undefined { + { sourceUrls: string[] } | undefined { const sourceUrls = this.playlistEpgUrls(); return sourceUrls.length > 0 ? { sourceUrls } : undefined; } diff --git a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html index d696a52ca..25cbb91fa 100644 --- a/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html +++ b/libs/ui/components/src/lib/channel-list-container/groups-view/groups-view.component.html @@ -1,325 +1,356 @@ -@if (!hasAnyGroups()) { -
- {{ - searchTerm() ? 'search_off' : 'folder_off' - }} -
- {{ - (searchTerm() - ? 'CHANNELS.NO_SEARCH_RESULTS' - : 'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.TITLE' - ) | translate - }} -
-
- {{ - (searchTerm() - ? 'CHANNELS.TRY_DIFFERENT_SEARCH' - : 'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.DESCRIPTION' - ) | translate - }} -
-
-} @else { -
-
+ chevron_right + + } + - - - -} + + + +
+ + {{ hasSearchQuery() ? 'search_off' : 'visibility_off' }} + +
+ {{ + (!hasAnyGroups() + ? 'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.TITLE' + : hasSearchQuery() + ? 'CHANNELS.NO_SEARCH_RESULTS' + : 'CHANNELS.NO_VISIBLE_GROUPS' + ) | translate + }} +
+
+ {{ + (!hasAnyGroups() + ? 'PORTALS.ERROR_VIEW.EMPTY_CATEGORY.DESCRIPTION' + : hasSearchQuery() + ? 'CHANNELS.TRY_DIFFERENT_SEARCH' + : 'CHANNELS.MANAGE_GROUPS_HINT' + ) | translate + }} +
+
+ } + +