Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/ui-app-overscroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
type: fix
area: ui
---

The dashboard and other scrollable pages stop bouncing at the top and bottom in the installed web app and Android TV app.
48 changes: 45 additions & 3 deletions apps/web-e2e/src/pwa-detail-overscroll.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { expect, test } from './fixtures';
import { configureMockProviderBackend } from './provider-target-route';
import {
addXtreamPortal,
DEFAULT_PASSWORD,
DEFAULT_USERNAME,
interceptXtreamRequests,
MOCK_SERVER,
} from './xtream-series-playback.fixture';

test('@xtream @pwa standalone series details stop vertical overscroll', async ({
test('@xtream @pwa standalone workspace and series details stop vertical overscroll', async ({
browserName,
baseURL,
request,
}) => {
test.skip(browserName !== 'chromium', 'Requires Chromium app mode');
const context = await chromium.launchPersistentContext('', {
Expand All @@ -29,13 +33,51 @@ test('@xtream @pwa standalone series details stop vertical overscroll', async ({
)
)
.toBe(true);
await expect(page).toHaveURL(/\/workspace\/dashboard$/);
await expect(page.locator('main.workspace-content')).toHaveCSS(
'overscroll-behavior-y',
'none'
);
await expect(page.locator('html')).toHaveCSS(
'overscroll-behavior-y',
'none'
);
await addXtreamPortal(page);
await page.getByRole('link', { name: 'Series', exact: true }).click();
await page.locator('app-grid-list mat-card').first().click();
await page.goto(page.url().replace(/\/vod.*$/, '/series'));
await expect(page).toHaveURL(/\/series$/);
const grid = page.locator('app-category-content-view app-grid-list');
await expect(grid).toHaveCSS('overscroll-behavior-y', 'none');
const response = await request.get(`${MOCK_SERVER}/player_api.php`, {
params: {
username: DEFAULT_USERNAME,
password: DEFAULT_PASSWORD,
action: 'get_series',
},
});
const [series] = (await response.json()) as {
category_id: string;
series_id: number;
}[];
await page.goto(
`${page.url()}/${series.category_id}/${series.series_id}`
);
const shell = page.locator('app-portal-detail-shell');
await expect(shell.getByRole('heading', { level: 1 })).toBeVisible();
await expect(shell).toHaveCSS('overscroll-behavior-y', 'none');
} finally {
await context.close();
}
});

test('@web browser dashboard keeps default overscroll', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveURL(/\/workspace\/dashboard$/);
await expect(page.locator('main.workspace-content')).toHaveCSS(
'overscroll-behavior-y',
'auto'
);
await expect(page.locator('html')).toHaveCSS(
'overscroll-behavior-y',
'auto'
);
});
13 changes: 12 additions & 1 deletion apps/web/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,28 @@ textarea {
}

@media (display-mode: standalone) {
html {
--app-overscroll-y: none;
}

html,
body {
user-select: none;
}

body {
overscroll-behavior-y: contain;
overflow: hidden;
}
}

html.android-tv-shell {
--app-overscroll-y: none;
}

:where(html, body, app-root > .app-viewport) {
overscroll-behavior-y: var(--app-overscroll-y, auto);
}

// ─── Utilities ────────────────────────────────────────────────────────────────
.mat-tab-label-active {
opacity: 1 !important;
Expand Down
12 changes: 9 additions & 3 deletions docs/architecture/iptvnator-ui-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Use it when changing existing views or introducing new list-based UI in the work

5. Scroll ownership must be explicit.
Headers stay visible. Lists scroll. Do not let nested panes compete for scroll.
The viewport and independent vertical page scrollers stop at their edges in
standalone PWAs and the native Android TV shell. The web stylesheet sets
`--app-overscroll-y` for those runtimes and applies it to the viewport.
The lazy workspace stylesheet applies it to routed scroll panes. Browser
tabs retain their normal behavior and horizontal rails still hand vertical
gestures to the page.

6. Normal text, including metadata and selected labels, keeps at least 4.5:1
contrast. Use `--app-muted-color` for secondary text and
Expand Down Expand Up @@ -432,9 +438,9 @@ TMDB enrichment adds a moment later fills the compact hero instead of
growing it. The pane is a size container (`detail`); the poster hides below
760px of pane width.

The detail shell owns vertical scrolling. In standalone PWA windows it stops
vertical overscroll at both edges so dragging past the content cannot expose a
gap beneath the workspace header.
The detail shell owns vertical scrolling and follows the app's overscroll
policy so dragging past the content cannot expose a gap beneath the workspace
header in standalone apps.

The pieces are shared and provider-neutral (`libs/ui/components/src/lib/detail-ui/`):
`app-meta-chip` (pill; `rating` and `status` variants; facets as projected
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@
clamp(28px, 4vw, 48px);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
overscroll-behavior-x: contain;
overscroll-behavior-y: var(--app-overscroll-y, contain);
}

.downloads__message {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@
}
}

@media (display-mode: standalone) {
:host {
overscroll-behavior-y: none;
}
}

:host-context(.dark-theme) {
--poster-shadow: 0 18px 40px rgba(0, 0, 0, 0.6);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ app-workspace-context-category-view {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
overscroll-behavior-x: contain;
overscroll-behavior-y: var(--app-overscroll-y, contain);
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--app-muted-color) 55%, transparent)
transparent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,30 @@ app-workspace-shell-context-sidebar {
min-width: 0;
min-height: 0;
overflow: auto;
overscroll-behavior-y: var(--app-overscroll-y, auto);
background: var(--app-content-bg, var(--mat-sys-surface));
}

// These scrollers live inside routed components. Keep their rules with the
// lazy workspace and let horizontal rails pass vertical gestures to the page.
:host
::ng-deep
:where(
.settings-nav__groups,
.app-scrollbar:not(.epg-timeline__ribbon),
app-category-view,
app-category-content-view app-grid-list,
app-search-layout .results-container,
app-unified-grid-tab .grid-container,
.epg-guide__viewport,
app-recently-added,
app-actor-view .actor-view,
app-discover-view .discover-view,
app-portal-detail-shell
) {
overscroll-behavior-y: var(--app-overscroll-y, auto);
}

// The offline banner has no stylesheet of its own: a component stylesheet
// adds a <style> to every launch, which the launch journey counts. Its
// elements are the banner's, hence ::ng-deep under this host.
Expand Down
4 changes: 4 additions & 0 deletions tv-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ APK

## What it does

The Android WebView and the PWA's vertical page scrollers stop at their scroll
edges. The native bridge activates the PWA rule, so browser remote emulation
keeps browser scrolling behavior.

Local playlist import and backup restore use Android's document picker, without
broad storage permissions. Cancellation leaves the file input unchanged; devices
without a document picker show an explanatory message. The shared parsers validate
Expand Down
1 change: 1 addition & 0 deletions tv-shell/android/app/src/main/assets/tvshell/tv.js
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,7 @@
// ------------------------------------------------------------- init
function init() {
document.documentElement.classList.add('tv-shell');
if (isAndroid) document.documentElement.classList.add('android-tv-shell');
document.body.classList.add('tv-shell');
installStyle();
installNetworkTracking();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ public void onClick(View v) {
showError(getString(R.string.webview_unavailable) + "\n" + e);
return;
}
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
root.addView(webView, 0, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
WebSettings s = webView.getSettings();
s.setJavaScriptEnabled(true);
Expand Down
Loading