Skip to content

Podcast: add locked-preview component for Episodes + Stats#48885

Draft
robertbpugh wants to merge 2 commits into
trunkfrom
pods/locked-preview
Draft

Podcast: add locked-preview component for Episodes + Stats#48885
robertbpugh wants to merge 2 commits into
trunkfrom
pods/locked-preview

Conversation

@robertbpugh
Copy link
Copy Markdown
Contributor

Fixes #

Proposed changes

Adds a richer locked-preview UX for free users on the Episodes tab and Stats panel, superseding the plain upsell card the upstream gate PRs ship today.

  • src/dashboard/locked-preview/index.tsx — the shell. Renders the variant's sample-data preview behind a 50% / 4px blur (aria-hidden), with a centered overlay card carrying the upgrade title, description, and primary CTA to Premium checkout.
  • episodes-preview.tsx — static 5-row episode table with thumbnail / title / duration / plays / date / status columns, mirroring the live DataViews shape.
  • stats-preview.tsx — static Downloads bar chart (CSS-only, no @automattic/charts dependency so free users don't pull the heavy stats chunk), Top episodes list, By app list.
  • style.scss — shell, blur layer, overlay card, and the two preview variants. Uses CSS logical properties throughout.

Accessibility

  • The sample data is aria-hidden="true" and contains no focusable elements, so screen readers and keyboard users skip it entirely.
  • The CTA receives autoFocus on mount so keyboard / screen reader users land on the upgrade affordance instead of placeholder data. (Per the spec's "focus trap on the CTA" — since the CTA is the only focusable element on the panel, focus naturally stays there; no synthetic trap needed.)
  • The wrapper has role="region" + aria-labelledby pointing at the overlay title so screen readers announce the locked-preview as a labeled region.
  • Escape blurs the active element so the user can tab back to the dashboard tab list rather than staying parked on the CTA.

What this PR does NOT change

The Episodes / Stats call sites aren't flipped here — both live in not-yet-merged upstream PRs:

Distribution and Settings are explicitly out of scope per the spec — not gated.

Dependencies

Per Rob's override (out-of-office): "If jetpack#48702 hasn't merged, open PR 8 as draft anyway. Add 'Depends on: #48702' to the description."

Related product discussion/links

  • Part of the /podcast UI polish batch.

Does this pull request change what data or activity we track or use?

No. The component renders only hardcoded sample data and a checkout link.

Testing instructions

Because the call sites aren't wired here, you can't visit /podcast and see the locked preview yet. Two options for visual review:

  1. Storybook or scratch route: mount <LockedPreview variant="episodes" /> / <LockedPreview variant="stats" /> somewhere in the dashboard temporarily to eyeball.
  2. Wait for Podcast: gate the Episodes dashboard tab on Premium product access (PODS-145) #48704 to merge, then apply the sample patch above and visit /podcast on a free / non-grandfathered site.

When you do view it:

  • Sample data should be visibly blurred and faded; not interactive on click.
  • Overlay card should be centered, with the title, description, and primary CTA.
  • Tabbing should land on the CTA only. Tab again should leave the panel.
  • Pressing Escape on the CTA should remove focus from it; pressing Tab from there should land on the next focusable element outside the panel (the dashboard tab list).
  • Screen reader: the region should be announced as labeled by the overlay title; the placeholder content should be silent.

Notes for reviewer:

  • I'm an automation that can't drive a browser, so I haven't captured screenshots. Visual verification still needs a sandbox check at review time.
  • The Stats placeholder is CSS-only on purpose — pulling @automattic/charts for free users is the kind of free-plan footgun this UX is supposed to avoid in the first place.

Spec excerpt (from the work order):

PR 8: Locked-state preview UX for Episodes and Stats tabs (free users)

What this PR ships

  • Episodes tab (free user): render a sample episode list of 4-6 placeholder rows behind a blur/opacity layer (about 50% opacity, 4px blur). Centered upgrade overlay reads "Episode dashboard included with Premium" with a primary CTA button to checkout.
  • Stats panel (free user): render the same chart layout a Premium user sees (Downloads bar chart, Top episodes list, By app list) with placeholder data behind the same blur/opacity layer. Same upgrade overlay style.
  • Distribution tab: NOT gated. No overlay.
  • Settings tab: NOT gated. No overlay.

Implementation

  • Look for an existing upsell component (likely under src/dashboard/upsell/ in the jetpack-mu-wpcom podcast surface). Either extend it with a preview-mode variant, or supersede it with src/dashboard/locked-preview/ and update the Episodes + Stats call sites.
  • Sample data is static, hardcoded. No network calls for free users.
  • Overlay must be keyboard-accessible: focus trap on the CTA, aria-label on the layer, escape returns to the dashboard.

The forthcoming Premium feature-gate PRs render a plain upsell card
for free users on the Episodes tab and on the Stats panel. This adds
a richer locked-preview component that supersedes that card:

- A sample-data preview (5 episode rows for Episodes; Downloads bar
  chart + Top episodes list + By app list for Stats) renders behind
  a 50%/4px blur and aria-hidden so it's purely visual.
- A centered overlay card carries the upgrade title, description, and
  primary CTA to Premium checkout.
- Keyboard: the CTA receives autoFocus on mount so a keyboard / screen
  reader user lands on the upgrade affordance, not on placeholder
  data; the wrapper handles Escape by blurring the active element so
  the user can tab back out to the dashboard tab list.
- Static data only — no network calls for free users; no
  @automattic/charts dependency in the Stats variant so free users
  don't pull the heavy stats chunk.

Component is added but call sites aren't flipped here: the Episodes
call site lives in #48704 and the Stats premiumRequired flag in #48703,
neither of which has merged yet. PR description spells out the
follow-up wiring.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label May 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 15, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the pods/locked-preview branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack pods/locked-preview
bin/jetpack-downloader test jetpack-mu-wpcom-plugin pods/locked-preview

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented May 15, 2026

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

@robertbpugh
Copy link
Copy Markdown
Contributor Author

Review verdict: READY (after fix)

Scope reviewed: branch diff against trunk — 5 files in projects/packages/podcast/src/dashboard/locked-preview/:

  • index.tsx — the LockedPreview component (variant: episodes | stats), checkout URL, escape-blur, hoisted i18n strings, autoFocus CTA.
  • episodes-preview.tsx — static sample-data preview rendered behind the blur.
  • stats-preview.tsx — static sample-data preview rendered behind the blur.
  • style.scss — blur + overlay + pointer-events: none on the sample.
  • changelog/pods-locked-preview — minor/added entry.

Fix applied this pass

Pass 1 CI failed on three ESLint findings in index.tsx:

  • @wordpress/no-global-active-element on document.activeElement (line 68).
  • jsx-a11y/no-noninteractive-element-interactions on the wrapper <div role="region" onKeyDown> (line 79).
  • Unused eslint-disable jsx-a11y/no-static-element-interactions directive (line 78, because the actual rule fired was the noninteractive variant).

Patched in f06d018e: added useRef<HTMLDivElement> for the wrapper and routed activeElement lookup through wrapperRef.current?.ownerDocument.activeElement. Switched the eslint-disable to jsx-a11y/no-noninteractive-element-interactions (the rule that actually triggers on the role="region" div). Both errors clear; the unused-directive warning resolves as a side effect. Codex Pass 2: clean.

Verification

  • Sample preview is aria-hidden="true" + pointer-events: none (in SCSS), so it stays off the a11y tree and unclickable.
  • autoFocus on the CTA is opt-in by intent (placement comment + lint suppression on the same line).
  • Hoisted i18n strings avoid the terser fold that the jetpack i18n-check rejects (__(cond?'a':'b') shape).
  • getProductCheckoutUrl( 'premium', siteSuffix, ... ) matches the helper signature in projects/js-packages/components/tools/get-product-checkout-url.ts; falls back to wordpress.com/pricing when siteSuffix is empty.

Severity

None CRITICAL, none MAJOR, none MINOR.

@robertbpugh robertbpugh marked this pull request as ready for review May 15, 2026 23:43
Copilot AI review requested due to automatic review settings May 15, 2026 23:43
@robertbpugh robertbpugh marked this pull request as draft May 15, 2026 23:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Podcast locked-preview UI intended for free users, showing blurred static Episodes/Stats previews with an upgrade CTA before the future gate call sites wire it in.

Changes:

  • Adds the LockedPreview shell with variant-specific overlay copy and checkout CTA.
  • Adds static Episodes and Stats preview components plus scoped SCSS.
  • Adds a package changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
projects/packages/podcast/src/dashboard/locked-preview/index.tsx Adds the shared locked-preview shell and CTA behavior.
projects/packages/podcast/src/dashboard/locked-preview/episodes-preview.tsx Adds a static blurred episode table preview.
projects/packages/podcast/src/dashboard/locked-preview/stats-preview.tsx Adds static CSS-only stats preview modules.
projects/packages/podcast/src/dashboard/locked-preview/style.scss Adds styles for the shell, overlay card, and preview variants.
projects/packages/podcast/changelog/pods-locked-preview Adds the changelog entry for the new component.
Comments suppressed due to low confidence (1)

projects/packages/podcast/src/dashboard/locked-preview/stats-preview.tsx:29

  • These visible sample counts are hard-coded with US comma separators. Please format numeric placeholder values through the same localization path used by the live stats UI (or use skeleton placeholders) so the preview does not show locale-specific number formatting.
const SAMPLE_BY_APP: SampleRow[] = [
	{ id: 1, label: 'Apple Podcasts', value: '2,140', pct: 100 },
	{ id: 2, label: 'Spotify', value: '1,420', pct: 66 },
	{ id: 3, label: 'Pocket Casts', value: '510', pct: 24 },
	{ id: 4, label: 'Overcast', value: '320', pct: 15 },

Significance: minor
Type: added

Add a locked-preview component for free users on the Episodes and Stats panels: a blurred sample of the gated content behind a centered upgrade overlay. Replaces the plain upsell card once the upstream feature-gate PRs land.
Comment on lines +69 to +72
const active = wrapperRef.current?.ownerDocument.activeElement;
if ( active instanceof HTMLElement ) {
active.blur();
}
Comment on lines +18 to +22
const SAMPLE_TOP_EPISODES: SampleRow[] = [
{ id: 1, label: 'Episode 1 — the first conversation', value: '1,284', pct: 100 },
{ id: 2, label: 'Episode 2 — guest interview with a friend', value: '973', pct: 76 },
{ id: 3, label: 'Episode 3 — back-to-basics, why we started', value: '812', pct: 63 },
{ id: 4, label: 'Episode 4 — Q&A from our listeners', value: '604', pct: 47 },
Comment on lines +17 to +56
const SAMPLE_EPISODES: SampleEpisode[] = [
{
id: 1,
title: 'Episode 1 — the first conversation',
duration: '42:18',
plays: '1,284',
date: 'May 1, 2026',
status: 'Published',
},
{
id: 2,
title: 'Episode 2 — guest interview with a friend',
duration: '38:02',
plays: '973',
date: 'May 8, 2026',
status: 'Published',
},
{
id: 3,
title: 'Episode 3 — back-to-basics, why we started',
duration: '51:47',
plays: '812',
date: 'May 15, 2026',
status: 'Published',
},
{
id: 4,
title: 'Episode 4 — Q&A from our listeners',
duration: '29:33',
plays: '604',
date: 'May 22, 2026',
status: 'Published',
},
{
id: 5,
title: 'Episode 5 — short bonus thoughts',
duration: '14:20',
plays: '450',
date: 'May 28, 2026',
status: 'Scheduled',
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Podcast [Status] In Progress [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants