mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
style(ui): the kit panel for every menu, shared danger rows, radio pickers (#1945)
* style(ui): the kit panel for every menu, shared danger rows, radio pickers Every mat-menu now gets the kit panel from one global rule instead of the opt-in `.app-menu` class: a bordered 10px panel, 36px rows at 13px with 16px icons, the checked row on the list-selection tokens. The mixin caps the panel at 360px wide and 480px tall (less in a short window), so long lists such as seasons and guide scopes scroll inside it; Material 22 sets no height limit. Destructive rows in the downloads lists and the dashboard rail card menu use `app-menu-item--danger` after a mat-divider. The rail menu's dead `panelClass` and private danger/separator rules are gone. The season picker's rows are appMenuItemRadio with the check slot, and its outer tablist role applies only in chip mode. The guide scope rows are menuitemradio with aria-checked and the same check icon, spelled out because ui-epg sits below ui-components. Both lose their private panel styles. The refine menu captions and the downloads source header use the kit's section label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(ui): replace the stale no-height-cap menu guidance The Single-Choice Menus section still said Material sets no max-height on a menu, so a tall menu is clipped. The kit panel now caps every menu at 480px and scrolls; the bullet keeps the advice to open a menu of actions whole and move a secondary group into a submenu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): park the pointer before the keyboard-only refine menu steps On the Linux and Windows runners the refine menu test found the rating row inactive after ArrowUp, with its submenu already open. With the kit's 36px rows that row sits under the runners' resting cursor; Chromium hovers whatever lands under a resting pointer after layout, Material opens a submenu on hover, and ArrowUp then moved inside the thresholds. Resting a pointer on the row locally reproduces the same failure. The test now moves the pointer to the corner before it drives the menu from the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8098ea8bb8
commit
ea1ac41fc9
28 files changed
+271
-142
No files matched your search
@@ -41,14 +41,25 @@
|
||||
>
|
||||
{{ scopeLabel() }} <mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<mat-menu #scopeMenu="matMenu" class="guide-toolbar__scope-menu">
|
||||
<!-- Single-choice rows, as `appMenuItemRadio` renders them in
|
||||
@iptvnator/ui/components, which depends on this library. -->
|
||||
<mat-menu #scopeMenu="matMenu">
|
||||
@for (scope of scopes(); track scope.id) {
|
||||
@let checked = scope.id === scopeId();
|
||||
<button
|
||||
mat-menu-item
|
||||
[class.is-selected]="scope.id === scopeId()"
|
||||
role="menuitemradio"
|
||||
[attr.aria-checked]="checked"
|
||||
(click)="scopeChange.emit(scope.id)"
|
||||
>
|
||||
{{ scope.label }}
|
||||
<mat-icon
|
||||
class="app-menu-item-radio-check"
|
||||
aria-hidden="true"
|
||||
[style.visibility]="checked ? null : 'hidden'"
|
||||
>
|
||||
check
|
||||
</mat-icon>
|
||||
<span>{{ scope.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
@@ -271,14 +271,3 @@
|
||||
color: $text-tertiary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// CDK menus render outside the component, so the scope menu needs a piercing
|
||||
// rule rather than component-scoped styling.
|
||||
::ng-deep .guide-toolbar__scope-menu {
|
||||
max-height: 60vh;
|
||||
|
||||
.is-selected {
|
||||
color: var(--app-selection-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { EpgGuideScope } from './epg-guide-source';
|
||||
import { EpgGuideToolbarComponent } from './epg-guide-toolbar.component';
|
||||
|
||||
const SCOPES: EpgGuideScope[] = [
|
||||
{ id: 'all', label: 'All channels', kind: 'all' },
|
||||
{ id: 'favorites', label: 'Favorites', kind: 'favorites' },
|
||||
{ id: 'group:news', label: 'News', kind: 'group' },
|
||||
];
|
||||
|
||||
describe('EpgGuideToolbarComponent scope menu', () => {
|
||||
function render(scopeId: string) {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [EpgGuideToolbarComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
currentLang: 'en',
|
||||
defaultLang: 'en',
|
||||
onLangChange: new BehaviorSubject(null),
|
||||
onTranslationChange: new BehaviorSubject(null),
|
||||
onDefaultLangChange: new BehaviorSubject(null),
|
||||
instant: (key: string) => key,
|
||||
get: (key: string) => of(key),
|
||||
stream: (key: string) => of(key),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const fixture = TestBed.createComponent(EpgGuideToolbarComponent);
|
||||
fixture.componentRef.setInput('dayMs', Date.UTC(2026, 9, 14));
|
||||
fixture.componentRef.setInput('scopes', SCOPES);
|
||||
fixture.componentRef.setInput('scopeId', scopeId);
|
||||
fixture.detectChanges();
|
||||
const trigger = fixture.nativeElement.querySelector(
|
||||
'.guide-toolbar__scope'
|
||||
) as HTMLButtonElement;
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
const rows = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'.cdk-overlay-container [mat-menu-item]'
|
||||
)
|
||||
);
|
||||
return { fixture, rows };
|
||||
}
|
||||
|
||||
it('marks the chosen scope as the checked single-choice row', () => {
|
||||
const { rows } = render('favorites');
|
||||
|
||||
expect(rows.map((row) => row.getAttribute('role'))).toEqual([
|
||||
'menuitemradio',
|
||||
'menuitemradio',
|
||||
'menuitemradio',
|
||||
]);
|
||||
expect(rows.map((row) => row.getAttribute('aria-checked'))).toEqual([
|
||||
'false',
|
||||
'true',
|
||||
'false',
|
||||
]);
|
||||
expect(rows.map((row) => row.textContent?.trim())).toEqual([
|
||||
'check All channels',
|
||||
'check Favorites',
|
||||
'check News',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows the check on the chosen row only, keeping every label on one edge', () => {
|
||||
const { rows } = render('group:news');
|
||||
|
||||
const checks = rows.map(
|
||||
(row) =>
|
||||
(row.querySelector('.app-menu-item-radio-check') as HTMLElement)
|
||||
.style.visibility
|
||||
);
|
||||
expect(checks).toEqual(['hidden', 'hidden', '']);
|
||||
});
|
||||
|
||||
it('reports the picked scope', () => {
|
||||
const { fixture, rows } = render('all');
|
||||
const picked: string[] = [];
|
||||
fixture.componentInstance.scopeChange.subscribe((id) =>
|
||||
picked.push(id)
|
||||
);
|
||||
|
||||
rows[2].click();
|
||||
|
||||
expect(picked).toEqual(['group:news']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user