Add skip links for content, player and footer. For #1826

This commit is contained in:
Gabe Kangas
2023-01-21 23:19:17 -08:00
parent b0f88519d0
commit cdaae66e94
7 changed files with 47 additions and 5 deletions

View File

@@ -11,6 +11,7 @@ import {
isChatAvailableSelector, isChatAvailableSelector,
clientConfigStateAtom, clientConfigStateAtom,
fatalErrorStateAtom, fatalErrorStateAtom,
appStateAtom,
} from '../../stores/ClientConfigStore'; } from '../../stores/ClientConfigStore';
import { Content } from '../../ui/Content/Content'; import { Content } from '../../ui/Content/Content';
import { Header } from '../../ui/Header/Header'; import { Header } from '../../ui/Header/Header';
@@ -22,6 +23,7 @@ import { ServerRenderedHydration } from '../../ServerRendered/ServerRenderedHydr
import { Theme } from '../../theme/Theme'; import { Theme } from '../../theme/Theme';
import styles from './Main.module.scss'; import styles from './Main.module.scss';
import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker'; import { PushNotificationServiceWorker } from '../../workers/PushNotificationServiceWorker/PushNotificationServiceWorker';
import { AppStateOptions } from '../../stores/application-state';
const lockBodyStyle = ` const lockBodyStyle = `
body { body {
@@ -46,9 +48,11 @@ export const Main: FC = () => {
const { name, title, customStyles } = clientConfig; const { name, title, customStyles } = clientConfig;
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector); const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom); const fatalError = useRecoilValue<DisplayableError>(fatalErrorStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const layoutRef = useRef<HTMLDivElement>(null); const layoutRef = useRef<HTMLDivElement>(null);
const { chatDisabled } = clientConfig; const { chatDisabled } = clientConfig;
const { videoAvailable } = appState;
useEffect(() => { useEffect(() => {
setupNoLinkReferrer(layoutRef.current); setupNoLinkReferrer(layoutRef.current);
@@ -137,7 +141,12 @@ export const Main: FC = () => {
<Script strategy="afterInteractive" src="/customjavascript" /> <Script strategy="afterInteractive" src="/customjavascript" />
<Layout ref={layoutRef} className={styles.layout}> <Layout ref={layoutRef} className={styles.layout}>
<Header name={title || name} chatAvailable={isChatAvailable} chatDisabled={chatDisabled} /> <Header
name={title || name}
chatAvailable={isChatAvailable}
chatDisabled={chatDisabled}
online={videoAvailable}
/>
<Content /> <Content />
{fatalError && ( {fatalError && (
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} /> <FatalErrorStateModal title={fatalError.title} message={fatalError.message} />

View File

@@ -115,7 +115,7 @@ const DesktopContent = ({
return ( return (
<> <>
<div className={styles.lowerHalf}> <div className={styles.lowerHalf} id="skip-to-content">
<ContentHeader <ContentHeader
name={name} name={name}
title={streamTitle} title={streamTitle}

View File

@@ -6,7 +6,7 @@ export type FooterProps = {
}; };
export const Footer: FC<FooterProps> = ({ version }) => ( export const Footer: FC<FooterProps> = ({ version }) => (
<footer className={styles.footer}> <footer className={styles.footer} id="footer">
<span> <span>
Powered by <a href="https://owncast.online">{version}</a> Powered by <a href="https://owncast.online">{version}</a>
</span> </span>

View File

@@ -42,3 +42,18 @@
overflow: hidden; overflow: hidden;
line-height: 1.4; line-height: 1.4;
} }
.skipLink {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skipLink:focus {
position: static;
width: auto;
height: auto;
}

View File

@@ -2,6 +2,7 @@ import { Tag, Tooltip } from 'antd';
import { FC } from 'react'; import { FC } from 'react';
import cn from 'classnames'; import cn from 'classnames';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Link from 'next/link';
import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo'; import { OwncastLogo } from '../../common/OwncastLogo/OwncastLogo';
import styles from './Header.module.scss'; import styles from './Header.module.scss';
@@ -18,14 +19,27 @@ export type HeaderComponentProps = {
name: string; name: string;
chatAvailable: boolean; chatAvailable: boolean;
chatDisabled: boolean; chatDisabled: boolean;
online: boolean;
}; };
export const Header: FC<HeaderComponentProps> = ({ export const Header: FC<HeaderComponentProps> = ({
name = 'Your stream title', name = 'Your stream title',
chatAvailable, chatAvailable,
chatDisabled, chatDisabled,
online,
}) => ( }) => (
<header className={cn([`${styles.header}`], 'global-header')}> <header className={cn([`${styles.header}`], 'global-header')}>
{online && (
<Link href="#player" className={styles.skipLink}>
Skip to player
</Link>
)}
<Link href="#skip-to-content" className={styles.skipLink}>
Skip to page content
</Link>
<Link href="#footer" className={styles.skipLink}>
Skip to footer
</Link>
<div className={styles.logo}> <div className={styles.logo}>
<div id="header-logo" className={styles.logoImage}> <div id="header-logo" className={styles.logoImage}>
<OwncastLogo variant="contrast" /> <OwncastLogo variant="contrast" />

View File

@@ -307,7 +307,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
); );
return ( return (
<div className={styles.container}> <div className={styles.container} id="player">
{online && ( {online && (
<div className={styles.player}> <div className={styles.player}>
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} /> <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />

View File

@@ -6,16 +6,20 @@ import {
currentUserAtom, currentUserAtom,
visibleChatMessagesSelector, visibleChatMessagesSelector,
clientConfigStateAtom, clientConfigStateAtom,
appStateAtom,
} from '../../../../components/stores/ClientConfigStore'; } from '../../../../components/stores/ClientConfigStore';
import Header from '../../../../components/ui/Header/Header'; import Header from '../../../../components/ui/Header/Header';
import { ClientConfig } from '../../../../interfaces/client-config.model'; import { ClientConfig } from '../../../../interfaces/client-config.model';
import { AppStateOptions } from '../../../../components/stores/application-state';
export default function ReadWriteChatEmbed() { export default function ReadWriteChatEmbed() {
const currentUser = useRecoilValue(currentUserAtom); const currentUser = useRecoilValue(currentUserAtom);
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector); const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom); const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
const { name, chatDisabled } = clientConfig; const { name, chatDisabled } = clientConfig;
const { videoAvailable } = appState;
if (!currentUser) { if (!currentUser) {
return null; return null;
@@ -26,7 +30,7 @@ export default function ReadWriteChatEmbed() {
return ( return (
<div> <div>
<ClientConfigStore /> <ClientConfigStore />
<Header name={name} chatAvailable chatDisabled={chatDisabled} /> <Header name={name} chatAvailable chatDisabled={chatDisabled} online={videoAvailable} />
<ChatContainer <ChatContainer
messages={messages} messages={messages}
usernameToHighlight={displayName} usernameToHighlight={displayName}