Skip to content
Open
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
76 changes: 56 additions & 20 deletions frontends/main/src/app-pages/PodcastPage/PodcastDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client"

import React, { useState, useEffect, useRef } from "react"
import { notFound } from "next/navigation"
import { useRouter } from "next-nprogress-bar"
import { Breadcrumbs, Typography, styled, useMediaQuery } from "ol-components"
import type { Theme } from "ol-components"
import { Button, ActionButton } from "@mitodl/smoot-design"
Expand All @@ -15,12 +17,11 @@ import { ResourceTypeEnum } from "api/v1"
import type { LearningResource } from "api/v1"
import moment from "moment"
import { formatDate } from "ol-utilities"
import { HOME } from "@/common/urls"
import { HOME, podcastEpisodePageView } from "@/common/urls"
import PodcastContainer from "./PodcastContainer"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import { useFeatureFlagEnabled } from "posthog-js/react"
import { FeatureFlags } from "@/common/feature_flags"
import { notFound } from "next/navigation"

const HeaderSection = styled.div(({ theme }) => ({
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
Expand Down Expand Up @@ -121,8 +122,10 @@ const HeaderTextContent = styled.div({

/* ── Episodes list ── */

const EpisodesSection = styled.div(({ theme }) => ({
padding: "0 48px",
const EpisodesSection = styled("div", {
shouldForwardProp: (prop) => prop !== "hasMoreEpisodes",
})<{ hasMoreEpisodes?: boolean }>(({ theme, hasMoreEpisodes }) => ({
padding: hasMoreEpisodes ? "0 48px" : "0 48px 40px 48px",
[theme.breakpoints.down("sm")]: {
padding: "0 0 48px",
},
Expand All @@ -148,17 +151,17 @@ const EpisodesHeading = styled(Typography)(({ theme }) => ({
},
}))

const EpisodeList = styled.ul({
listStyle: "none",
const EpisodeList = styled.div({
margin: 0,
Comment on lines 152 to 155
padding: 0,
display: "grid",
gridTemplateColumns: "1fr",
})

const EpisodeRow = styled("li", {
const EpisodeRow = styled("div", {
shouldForwardProp: (prop) => prop !== "isEpisodePage",
})<{ isEpisodePage?: boolean }>(({ theme, isEpisodePage }) => ({
cursor: "pointer",
margin: 0,
display: "flex",
flexDirection: "row",
Expand All @@ -182,6 +185,10 @@ const EpisodeRow = styled("li", {
backgroundColor: theme.custom.colors.lightGray1,
cursor: "pointer",
},
"&:focus-visible": {
outline: `2px solid ${theme.custom.colors.red}`,
outlineOffset: "-2px",
},
"&:hover .episode-title, &:focus-visible .episode-title": {
color: theme.custom.colors.red,
},
Expand Down Expand Up @@ -310,6 +317,7 @@ const PlayButton = styled(ActionButton, {

export type EpisodeItemProps = {
episode: LearningResource
href: string
onPlayClick: (episode: LearningResource) => void
onPauseClick?: () => void
isPlaying: boolean
Expand All @@ -319,12 +327,18 @@ export type EpisodeItemProps = {

export const EpisodeItem: React.FC<EpisodeItemProps> = ({
episode,
href,
onPlayClick,
onPauseClick,
isPlaying,
isPlayable,
isEpisodePage = false,
}) => {
const router = useRouter()

const handleRowNavigate = () => {
if (href) router.push(href)
}
const podcastEpisode =
episode.resource_type === "podcast_episode" ? episode.podcast_episode : null

Expand All @@ -340,7 +354,15 @@ export const EpisodeItem: React.FC<EpisodeItemProps> = ({

return (
<EpisodeRow
onClick={() => (isPlaying ? onPauseClick?.() : onPlayClick(episode))}
onClick={handleRowNavigate}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleRowNavigate()
}
}}
role="link"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

role="link" semantically implies an href attribute (like an anchor). Screen readers will announce it as a link but there's no associated URL in the accessible tree, and AT-users can't copy/open-in-new-tab.

Instead render EpisodeRow as an tag passing href directly, and remove the manual role, tabIndex, onClick, and onKeyDown, the browser handles all of that natively.

tabIndex={0}
isEpisodePage={isEpisodePage}
>
<EpisodeInfo>
Expand All @@ -364,6 +386,15 @@ export const EpisodeItem: React.FC<EpisodeItemProps> = ({
aria-label={
isPlaying ? `Pause ${episode.title}` : `Play ${episode.title}`
}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
if (isPlaying) {
onPauseClick?.()
} else {
onPlayClick(episode)
}
}}
isPlaying={isPlaying}
disabled={!isPlayable}
variant="secondary"
Expand Down Expand Up @@ -566,22 +597,27 @@ export const PodcastDetailPage: React.FC<PodcastDetailPageProps> = ({
</HeaderSection>

<PodcastContainer>
<EpisodesSection>
<EpisodesSection hasMoreEpisodes={!!hasNextPage}>
<EpisodesHeading variant="subtitle3">Episodes</EpisodesHeading>

{episodes && episodes.length > 0 && (
<EpisodeList>
<EpisodeList role="list">
{episodes.map((episode) => (
<EpisodeItem
key={episode.id}
episode={episode}
onPlayClick={handlePlayClick}
onPauseClick={() => playerRef.current?.pause()}
isPlaying={
playingEpisode?.id === episode.id && isAudioPlaying
}
isPlayable={Boolean(getEpisodeAudioUrl(episode))}
/>
<div key={episode.id} role="listitem">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

EpisodeList is now a div role="list", and each item is wrapped in <div role="listitem"> containing <EpisodeItem> which itself renders EpisodeRow (another div). This adds an extra DOM node with no purpose.

if you change:

{episodes.map((episode) => (
  <div key={episode.id} role="listitem">
    <EpisodeItem
      episode={episode}
      href={...}
      ...
    />
  </div>
))}

to

{episodes.map((episode) => (
  <EpisodeItem
    key={episode.id}
    episode={episode}
    href={...}
    ...
  />
))}

and add role="listitem" to EpisodeRow that eliminates the extra div wrapper with no semantic purpose

<EpisodeItem
episode={episode}
href={podcastEpisodePageView(
String(episode.id),
String(id),
)}
onPlayClick={handlePlayClick}
onPauseClick={() => playerRef.current?.pause()}
isPlaying={
playingEpisode?.id === episode.id && isAudioPlaying
}
isPlayable={Boolean(getEpisodeAudioUrl(episode))}
/>
</div>
))}
</EpisodeList>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ResourceTypeEnum } from "api/v1"
import type { LearningResource } from "api/v1"
import moment from "moment"
import { formatDate } from "ol-utilities"
import { HOME, podcastPageView } from "@/common/urls"
import { HOME, podcastPageView, podcastEpisodePageView } from "@/common/urls"
import DOMPurify from "isomorphic-dompurify"
import { EpisodeItem } from "./PodcastDetailPage"
import PodcastContainer from "./PodcastContainer"
Expand All @@ -47,9 +47,13 @@ const PageSection = styled.div(({ theme }) => ({
minHeight: "100vh",
}))

const HeaderSection = styled.div(({ theme }) => ({
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
marginBottom: "64px",
const HeaderSection = styled("div", {
shouldForwardProp: (prop) => prop !== "hasEpisodes",
})<{ hasEpisodes?: boolean }>(({ theme, hasEpisodes }) => ({
...(hasEpisodes && {
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
marginBottom: "64px",
}),
paddingBottom: "64px",
[theme.breakpoints.down("sm")]: {
marginBottom: "24px",
Expand Down Expand Up @@ -305,7 +309,7 @@ export const PodcastEpisodeDetailPage: React.FC<
/>
</PodcastContainer>
</BreadcrumbBar>
<HeaderSection>
<HeaderSection hasEpisodes={episodes.length > 0}>
<EpisodeContainer>
{podcast?.title && (
<EpisodeLabel href={podcastHref}>{podcast.title}</EpisodeLabel>
Expand Down Expand Up @@ -354,6 +358,11 @@ export const PodcastEpisodeDetailPage: React.FC<
<EpisodeItem
key={episode.id}
episode={episode}
href={
podcastId
? podcastEpisodePageView(String(episode.id), podcastId)
: ""
}
isPlaying={
playingEpisode?.id === episode.id && isAudioPlaying
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const VideoPlaylistCollectionPage: React.FC<
return notFound()
}
const isLoading = playlistLoading || itemsLoading
const totalCount = itemsData?.pages[0]?.count ?? 0
const videos = (
itemsData?.pages.flatMap((page) =>
page.results.map((rel) => rel.resource),
Expand All @@ -117,7 +118,6 @@ const VideoPlaylistCollectionPage: React.FC<
)
const playlistType = isOcwPlaylist(playlist)

const totalVideos = videos.length
const totalVideoSeconds = videos.reduce((acc, video) => {
const duration = video.video?.duration ?? ""
const match = /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/.exec(duration)
Expand All @@ -141,7 +141,7 @@ const VideoPlaylistCollectionPage: React.FC<
<VideoPageHeader
playlist={playlist}
isSeries={playlistType}
totalVideos={videos.length}
totalVideos={totalCount}
/>

{isLoading ? (
Expand All @@ -151,7 +151,7 @@ const VideoPlaylistCollectionPage: React.FC<
video={videos[0]}
href={getVideoHref(videos[0])}
isSeries={playlistType}
totalVideos={totalVideos}
totalVideos={totalCount}
totalTime={totalTime}
/>
) : null}
Expand Down
Loading