#6470 Improve accessibility and some minor css fixes
This commit is contained in:
commit
0cdf881941
37 changed files with 385 additions and 168 deletions
|
@ -2057,7 +2057,16 @@
|
||||||
"Only select creators can receive tips at this time": "Only select creators can receive tips at this time",
|
"Only select creators can receive tips at this time": "Only select creators can receive tips at this time",
|
||||||
"The payment will be made from your saved card": "The payment will be made from your saved card",
|
"The payment will be made from your saved card": "The payment will be made from your saved card",
|
||||||
"Commenting...": "Commenting...",
|
"Commenting...": "Commenting...",
|
||||||
|
"Show %count% replies": "Show %count% replies",
|
||||||
|
"Show reply": "Show reply",
|
||||||
|
"added to": "added to",
|
||||||
|
"removed from": "removed from",
|
||||||
|
"Skip Navigation": "Skip Navigation",
|
||||||
"Reset": "Reset",
|
"Reset": "Reset",
|
||||||
"Reset to original (previous) publish date": "Reset to original (previous) publish date",
|
"Reset to original (previous) publish date": "Reset to original (previous) publish date",
|
||||||
|
"%title% by %channelTitle%": "%title% by %channelTitle%",
|
||||||
|
"%title% by %channelTitle% %ariaDate%": "%title% by %channelTitle% %ariaDate%",
|
||||||
|
"%title% by %channelTitle% %ariaDate%, %mediaDuration%": "%title% by %channelTitle% %ariaDate%, %mediaDuration%",
|
||||||
|
"Search for something...": "Search for something...",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,6 +285,11 @@ function App(props: Props) {
|
||||||
}
|
}
|
||||||
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
|
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// $FlowFixMe
|
||||||
|
document.documentElement.setAttribute('lang', language);
|
||||||
|
}, [language]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!languages.includes(language)) {
|
if (!languages.includes(language)) {
|
||||||
setLanguage(language);
|
setLanguage(language);
|
||||||
|
|
|
@ -97,6 +97,9 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
const combinedRef = useCombinedRefs(ref, innerRef, myref);
|
const combinedRef = useCombinedRefs(ref, innerRef, myref);
|
||||||
const size = iconSize || (!label && !children) ? 18 : undefined; // Fall back to default
|
const size = iconSize || (!label && !children) ? 18 : undefined; // Fall back to default
|
||||||
|
|
||||||
|
// Label can be a string or object ( use title instead )
|
||||||
|
const ariaLabel = description || (typeof label === 'string' ? label : title);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<span className="button__content">
|
<span className="button__content">
|
||||||
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
||||||
|
@ -150,6 +153,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</a>
|
</a>
|
||||||
|
@ -196,6 +200,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
disabled={disable}
|
disabled={disable}
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
activeClassName={activeClass}
|
activeClassName={activeClass}
|
||||||
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -216,6 +221,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}}
|
}}
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
activeClassName={activeClass}
|
activeClassName={activeClass}
|
||||||
|
aria-label={ariaLabel}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
@ -224,7 +230,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
<button
|
<button
|
||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
title={title || defaultTooltip}
|
title={title || defaultTooltip}
|
||||||
aria-label={description || label || title}
|
aria-label={ariaLabel}
|
||||||
className={combinedClassName}
|
className={combinedClassName}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {
|
||||||
doCollectionEdit,
|
doCollectionEdit,
|
||||||
makeSelectUrlsForCollectionId,
|
makeSelectUrlsForCollectionId,
|
||||||
makeSelectIndexForUrlInCollection,
|
makeSelectIndexForUrlInCollection,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectDateForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
|
@ -22,11 +24,21 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ClaimPreview from './view';
|
|
||||||
|
|
||||||
const select = (state, props) => ({
|
import ClaimPreview from './view';
|
||||||
|
import formatMediaDuration from 'util/formatMediaDuration';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
const claim = props.uri && makeSelectClaimForUri(props.uri)(state);
|
||||||
|
const media = claim && claim.value && (claim.value.video || claim.value.audio);
|
||||||
|
const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
claim,
|
||||||
|
mediaDuration,
|
||||||
|
date: props.uri && makeSelectDateForUri(props.uri)(state),
|
||||||
|
title: props.uri && makeSelectTitleForUri(props.uri)(state),
|
||||||
pending: props.uri && makeSelectClaimIsPending(props.uri)(state),
|
pending: props.uri && makeSelectClaimIsPending(props.uri)(state),
|
||||||
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
|
||||||
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
|
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
|
||||||
obscureNsfw: selectShowMatureContent(state) === false,
|
obscureNsfw: selectShowMatureContent(state) === false,
|
||||||
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
@ -47,7 +59,8 @@ const select = (state, props) => ({
|
||||||
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
|
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||||
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state),
|
||||||
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),
|
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state),
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import React, { useEffect, forwardRef } from 'react';
|
import React, { useEffect, forwardRef } from 'react';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
|
import { isEmpty } from 'util/object';
|
||||||
import { lazyImport } from 'util/lazyImport';
|
import { lazyImport } from 'util/lazyImport';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { isEmpty } from 'util/object';
|
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||||
import FileThumbnail from 'component/fileThumbnail';
|
import FileThumbnail from 'component/fileThumbnail';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||||
|
@ -85,6 +86,8 @@ type Props = {
|
||||||
collectionUris: Array<Collection>,
|
collectionUris: Array<Collection>,
|
||||||
collectionIndex?: number,
|
collectionIndex?: number,
|
||||||
disableNavigation?: boolean,
|
disableNavigation?: boolean,
|
||||||
|
mediaDuration?: string,
|
||||||
|
date?: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
@ -99,8 +102,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
// claim properties
|
// claim properties
|
||||||
// is the claim consider nsfw?
|
// is the claim consider nsfw?
|
||||||
nsfw,
|
nsfw,
|
||||||
|
date,
|
||||||
|
title,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
streamingUrl,
|
streamingUrl,
|
||||||
|
mediaDuration,
|
||||||
// user properties
|
// user properties
|
||||||
channelIsBlocked,
|
channelIsBlocked,
|
||||||
hasVisitedUri,
|
hasVisitedUri,
|
||||||
|
@ -175,6 +181,21 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
|
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
|
||||||
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
|
|
||||||
|
// Get channel title ( use name as fallback )
|
||||||
|
let channelTitle = null;
|
||||||
|
if (signingChannel) {
|
||||||
|
const { value, name } = signingChannel;
|
||||||
|
if (value && value.title) {
|
||||||
|
channelTitle = value.title;
|
||||||
|
} else {
|
||||||
|
channelTitle = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aria-label value for claim preview
|
||||||
|
let ariaLabelData = isChannelUri ? title : formatClaimPreviewTitle(title, channelTitle, date, mediaDuration);
|
||||||
|
|
||||||
let navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/');
|
let navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/');
|
||||||
if (listId) {
|
if (listId) {
|
||||||
const collectionParams = new URLSearchParams();
|
const collectionParams = new URLSearchParams();
|
||||||
|
@ -313,18 +334,18 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isChannelUri && claim ? (
|
{isChannelUri && claim ? (
|
||||||
<UriIndicator uri={uri} link>
|
<UriIndicator focusable={false} uri={uri} link>
|
||||||
<ChannelThumbnail uri={uri} small={type === 'inline'} />
|
<ChannelThumbnail uri={uri} small={type === 'inline'} />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{!pending ? (
|
{!pending ? (
|
||||||
<NavLink {...navLinkProps}>
|
<NavLink aria-hidden tabIndex={-1} {...navLinkProps}>
|
||||||
<FileThumbnail thumbnail={thumbnailUrl}>
|
<FileThumbnail thumbnail={thumbnailUrl}>
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
{claim && !isCollection && (
|
{claim && !isCollection && (
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton hideDownloadStatus />
|
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton hideDownloadStatus />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
@ -335,7 +356,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
)}
|
)}
|
||||||
{isPlayable && (
|
{isPlayable && (
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
<FileWatchLaterLink uri={uri} />
|
<FileWatchLaterLink focusable={false} uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FileThumbnail>
|
</FileThumbnail>
|
||||||
|
@ -352,7 +373,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
{pending ? (
|
{pending ? (
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
) : (
|
) : (
|
||||||
<NavLink aria-current={active && 'page'} {...navLinkProps}>
|
<NavLink aria-label={ariaLabelData} aria-current={active ? 'page' : null} {...navLinkProps}>
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
@ -451,9 +472,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hideMenu && (
|
{!hideMenu && <ClaimMenuList uri={uri} collectionId={listId} />}
|
||||||
<ClaimMenuList uri={uri} collectionId={listId} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
</WrapperElement>
|
</WrapperElement>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,14 +9,23 @@ import {
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
makeSelectClaimIsStreamPlaceholder,
|
makeSelectClaimIsStreamPlaceholder,
|
||||||
|
makeSelectDateForUri,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import ClaimPreviewTile from './view';
|
import ClaimPreviewTile from './view';
|
||||||
|
import formatMediaDuration from 'util/formatMediaDuration';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
const claim = props.uri && makeSelectClaimForUri(props.uri)(state);
|
||||||
|
const media = claim && claim.value && (claim.value.video || claim.value.audio);
|
||||||
|
const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true });
|
||||||
|
|
||||||
|
return {
|
||||||
|
claim,
|
||||||
|
mediaDuration,
|
||||||
|
date: props.uri && makeSelectDateForUri(props.uri)(state),
|
||||||
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
|
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
|
||||||
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
|
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
|
||||||
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
@ -27,7 +36,8 @@ const select = (state, props) => ({
|
||||||
showMature: selectShowMatureContent(state),
|
showMature: selectShowMatureContent(state),
|
||||||
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
isMature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||||
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
|
@ -22,7 +23,9 @@ import PlaceholderTx from 'static/img/placeholderTx.gif';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
date?: any,
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
|
mediaDuration?: string,
|
||||||
resolveUri: (string) => void,
|
resolveUri: (string) => void,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
history: { push: (string) => void },
|
history: { push: (string) => void },
|
||||||
|
@ -54,6 +57,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
const {
|
const {
|
||||||
history,
|
history,
|
||||||
uri,
|
uri,
|
||||||
|
date,
|
||||||
isResolvingUri,
|
isResolvingUri,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
title,
|
title,
|
||||||
|
@ -73,6 +77,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
isLivestream,
|
isLivestream,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
mediaDuration,
|
||||||
} = props;
|
} = props;
|
||||||
const isRepost = claim && claim.repost_channel_url;
|
const isRepost = claim && claim.repost_channel_url;
|
||||||
const isCollection = claim && claim.value_type === 'collection';
|
const isCollection = claim && claim.value_type === 'collection';
|
||||||
|
@ -115,6 +120,10 @@ function ClaimPreviewTile(props: Props) {
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
const isChannel = claim && claim.value_type === 'channel';
|
const isChannel = claim && claim.value_type === 'channel';
|
||||||
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
|
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
|
||||||
|
const channelTitle = signingChannel && (signingChannel.value.title || signingChannel.name);
|
||||||
|
|
||||||
|
// Aria-label value for claim preview
|
||||||
|
let ariaLabelData = isChannel ? title : formatClaimPreviewTitle(title, channelTitle, date, mediaDuration);
|
||||||
|
|
||||||
function handleClick(e) {
|
function handleClick(e) {
|
||||||
if (navigateUrl) {
|
if (navigateUrl) {
|
||||||
|
@ -189,28 +198,27 @@ function ClaimPreviewTile(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
role="link"
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={classnames('card claim-preview--tile', {
|
className={classnames('card claim-preview--tile', {
|
||||||
'claim-preview__wrapper--channel': isChannel,
|
'claim-preview__wrapper--channel': isChannel,
|
||||||
'claim-preview__live': live,
|
'claim-preview__live': live,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<NavLink {...navLinkProps}>
|
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||||
<FileThumbnail thumbnail={thumbnailUrl} allowGifs>
|
<FileThumbnail thumbnail={thumbnailUrl} allowGifs>
|
||||||
{!isChannel && (
|
{!isChannel && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{/* @if TARGET='app' */}
|
{/* @if TARGET='app' */}
|
||||||
{isStream && (
|
{isStream && (
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton />
|
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
{isPlayable && (
|
{isPlayable && (
|
||||||
<div className="claim-preview__hover-actions">
|
<div className="claim-preview__hover-actions">
|
||||||
<FileWatchLaterLink uri={uri} />
|
<FileWatchLaterLink focusable={false} uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -228,7 +236,8 @@ function ClaimPreviewTile(props: Props) {
|
||||||
)}
|
)}
|
||||||
</FileThumbnail>
|
</FileThumbnail>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink {...navLinkProps}>
|
<div className="claim-tile__header">
|
||||||
|
<NavLink aria-label={ariaLabelData} {...navLinkProps}>
|
||||||
<h2 className="claim-tile__title">
|
<h2 className="claim-tile__title">
|
||||||
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
|
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
|
||||||
{isChannel && (
|
{isChannel && (
|
||||||
|
@ -236,9 +245,10 @@ function ClaimPreviewTile(props: Props) {
|
||||||
<UriIndicator uri={uri} />
|
<UriIndicator uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ClaimMenuList uri={uri} collectionId={listId} />
|
|
||||||
</h2>
|
</h2>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="claim-tile__info">
|
<div className="claim-tile__info">
|
||||||
{isChannel ? (
|
{isChannel ? (
|
||||||
|
@ -247,7 +257,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<UriIndicator uri={uri} link hideAnonymous>
|
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||||
<ChannelThumbnail uri={channelUri} xsmall />
|
<ChannelThumbnail uri={channelUri} xsmall />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,15 @@ const buildIcon = (iconStrokes: React$Node, customSvgValues = {}) =>
|
||||||
export const icons = {
|
export const icons = {
|
||||||
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
||||||
[ICONS.LBRY]: (props: IconProps) => (
|
[ICONS.LBRY]: (props: IconProps) => (
|
||||||
<svg stroke="currentColor" fill="currentColor" x="0px" y="0px" viewBox="0 0 322 254" className="icon lbry-icon">
|
<svg
|
||||||
|
{...props}
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="currentColor"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 322 254"
|
||||||
|
className="icon lbry-icon"
|
||||||
|
>
|
||||||
<path d="M296,85.9V100l-138.8,85.3L52.6,134l0.2-7.9l104,51.2L289,96.1v-5.8L164.2,30.1L25,116.2v38.5l131.8,65.2 l137.6-84.4l3.9,6l-141.1,86.4L18.1,159.1v-46.8l145.8-90.2C163.9,22.1,296,85.9,296,85.9z" />
|
<path d="M296,85.9V100l-138.8,85.3L52.6,134l0.2-7.9l104,51.2L289,96.1v-5.8L164.2,30.1L25,116.2v38.5l131.8,65.2 l137.6-84.4l3.9,6l-141.1,86.4L18.1,159.1v-46.8l145.8-90.2C163.9,22.1,296,85.9,296,85.9z" />
|
||||||
<path d="M294.3,150.9l2-12.6l-12.2-2.1l0.8-4.9l17.1,2.9l-2.8,17.5L294.3,150.9L294.3,150.9z" />
|
<path d="M294.3,150.9l2-12.6l-12.2-2.1l0.8-4.9l17.1,2.9l-2.8,17.5L294.3,150.9L294.3,150.9z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -73,6 +73,7 @@ class IconComponent extends React.PureComponent<Props> {
|
||||||
size={size || (sectionIcon ? 20 : 16)}
|
size={size || (sectionIcon ? 20 : 16)}
|
||||||
className={classnames(`icon icon--${icon}`, className, { 'color-override': iconColor })}
|
className={classnames(`icon icon--${icon}`, className, { 'color-override': iconColor })}
|
||||||
color={color}
|
color={color}
|
||||||
|
aria-hidden
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
downloading: boolean,
|
downloading: boolean,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
|
focusable: boolean,
|
||||||
fileInfo: ?FileListItem,
|
fileInfo: ?FileListItem,
|
||||||
openModal: (id: string, { path: string }) => void,
|
openModal: (id: string, { path: string }) => void,
|
||||||
pause: () => void,
|
pause: () => void,
|
||||||
|
@ -35,6 +36,7 @@ function FileDownloadLink(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
claim,
|
claim,
|
||||||
buttonType,
|
buttonType,
|
||||||
|
focusable = true,
|
||||||
showLabel = false,
|
showLabel = false,
|
||||||
hideOpenButton = false,
|
hideOpenButton = false,
|
||||||
hideDownloadStatus = false,
|
hideDownloadStatus = false,
|
||||||
|
@ -91,6 +93,8 @@ function FileDownloadLink(props: Props) {
|
||||||
pause();
|
pause();
|
||||||
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { path: fileInfo.download_path, isMine: claimIsMine });
|
openModal(MODALS.CONFIRM_EXTERNAL_RESOURCE, { path: fileInfo.download_path, isMine: claimIsMine });
|
||||||
}}
|
}}
|
||||||
|
aria-hidden={!focusable}
|
||||||
|
tabIndex={focusable ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -105,6 +109,8 @@ function FileDownloadLink(props: Props) {
|
||||||
icon={ICONS.DOWNLOAD}
|
icon={ICONS.DOWNLOAD}
|
||||||
label={showLabel ? label : null}
|
label={showLabel ? label : null}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
|
aria-hidden={!focusable}
|
||||||
|
tabIndex={focusable ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ type Props = {
|
||||||
index: number,
|
index: number,
|
||||||
length: number,
|
length: number,
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
push: string => void,
|
push: (string) => void,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ function FileDrop(props: Props) {
|
||||||
const navigationTimer = React.useRef(null);
|
const navigationTimer = React.useRef(null);
|
||||||
|
|
||||||
// Gets respective icon given a mimetype
|
// Gets respective icon given a mimetype
|
||||||
const getFileIcon = type => {
|
const getFileIcon = (type) => {
|
||||||
// Not all files have a type
|
// Not all files have a type
|
||||||
if (!type) return ICONS.FILE;
|
if (!type) return ICONS.FILE;
|
||||||
// Detect common types
|
// Detect common types
|
||||||
|
@ -77,10 +77,13 @@ function FileDrop(props: Props) {
|
||||||
}, [navigateToPublish]);
|
}, [navigateToPublish]);
|
||||||
|
|
||||||
// Handle file selection
|
// Handle file selection
|
||||||
const handleFileSelected = React.useCallback((selectedFile) => {
|
const handleFileSelected = React.useCallback(
|
||||||
|
(selectedFile) => {
|
||||||
updatePublishForm({ filePath: selectedFile });
|
updatePublishForm({ filePath: selectedFile });
|
||||||
hideDropArea();
|
hideDropArea();
|
||||||
}, [updatePublishForm, hideDropArea]);
|
},
|
||||||
|
[updatePublishForm, hideDropArea]
|
||||||
|
);
|
||||||
|
|
||||||
// Clear timers when unmounted
|
// Clear timers when unmounted
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -114,12 +117,12 @@ function FileDrop(props: Props) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) {
|
if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) {
|
||||||
getTree(dropData)
|
getTree(dropData)
|
||||||
.then(entries => {
|
.then((entries) => {
|
||||||
if (entries && entries.length) {
|
if (entries && entries.length) {
|
||||||
setFiles(entries);
|
setFiles(entries);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
setError(error || true);
|
setError(error || true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -146,7 +149,7 @@ function FileDrop(props: Props) {
|
||||||
const show = files.length === 1 || (!target && drag && (!modal || modal.id !== MODALS.FILE_SELECTION));
|
const show = files.length === 1 || (!target && drag && (!modal || modal.id !== MODALS.FILE_SELECTION));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames('file-drop', show && 'file-drop--show')}>
|
<div aria-hidden={!show} className={classnames('file-drop', show && 'file-drop--show')}>
|
||||||
<div className={classnames('card', 'file-drop__area')}>
|
<div className={classnames('card', 'file-drop__area')}>
|
||||||
<Icon size={64} icon={icon} className={'main-icon'} />
|
<Icon size={64} icon={icon} className={'main-icon'} />
|
||||||
<p>{target ? target.name : __(`Drop here to publish!`)} </p>
|
<p>{target ? target.name : __(`Drop here to publish!`)} </p>
|
||||||
|
|
|
@ -8,13 +8,14 @@ import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
|
focusable: boolean,
|
||||||
hasClaimInWatchLater: boolean,
|
hasClaimInWatchLater: boolean,
|
||||||
doToast: ({ message: string }) => void,
|
doToast: ({ message: string }) => void,
|
||||||
doCollectionEdit: (string, any) => void,
|
doCollectionEdit: (string, any) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function FileWatchLaterLink(props: Props) {
|
function FileWatchLaterLink(props: Props) {
|
||||||
const { claim, hasClaimInWatchLater, doToast, doCollectionEdit } = props;
|
const { claim, hasClaimInWatchLater, doToast, doCollectionEdit, focusable = true } = props;
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
let isHovering = useHover(buttonRef);
|
let isHovering = useHover(buttonRef);
|
||||||
|
|
||||||
|
@ -51,6 +52,8 @@ function FileWatchLaterLink(props: Props) {
|
||||||
(isHovering ? ICONS.COMPLETED : ICONS.TIME)
|
(isHovering ? ICONS.COMPLETED : ICONS.TIME)
|
||||||
}
|
}
|
||||||
onClick={(e) => handleWatchLater(e)}
|
onClick={(e) => handleWatchLater(e)}
|
||||||
|
aria-hidden={!focusable}
|
||||||
|
tabIndex={focusable ? 0 : -1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useIsMobile } from 'effects/use-screensize';
|
||||||
import NotificationBubble from 'component/notificationBubble';
|
import NotificationBubble from 'component/notificationBubble';
|
||||||
import NotificationHeaderButton from 'component/notificationHeaderButton';
|
import NotificationHeaderButton from 'component/notificationHeaderButton';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
|
import SkipNavigationButton from 'component/skipNavigationButton';
|
||||||
import Logo from 'component/logo';
|
import Logo from 'component/logo';
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
|
@ -235,6 +236,7 @@ const Header = (props: Props) => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="header__navigation">
|
<div className="header__navigation">
|
||||||
|
<SkipNavigationButton />
|
||||||
{!authHeader && (
|
{!authHeader && (
|
||||||
<span style={{ position: 'relative' }}>
|
<span style={{ position: 'relative' }}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -245,6 +247,7 @@ const Header = (props: Props) => {
|
||||||
}
|
}
|
||||||
className="header__navigation-item menu__title header__navigation-item--icon"
|
className="header__navigation-item menu__title header__navigation-item--icon"
|
||||||
icon={ICONS.MENU}
|
icon={ICONS.MENU}
|
||||||
|
aria-expanded={sidebarOpen}
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
>
|
>
|
||||||
{isAbsoluteSideNavHidden && isMobile && notificationsEnabled && <NotificationBubble />}
|
{isAbsoluteSideNavHidden && isMobile && notificationsEnabled && <NotificationBubble />}
|
||||||
|
@ -252,6 +255,7 @@ const Header = (props: Props) => {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
aria-label={__('Home')}
|
||||||
className="header__navigation-item header__navigation-item--lbry"
|
className="header__navigation-item header__navigation-item--lbry"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (history.location.pathname === '/') window.location.reload();
|
if (history.location.pathname === '/') window.location.reload();
|
||||||
|
|
|
@ -193,7 +193,7 @@ export default function Notification(props: Props) {
|
||||||
|
|
||||||
<div className="notification__menu">
|
<div className="notification__menu">
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton onClick={(e) => e.stopPropagation()}>
|
<MenuButton className={'menu-button notification__menu-button'} onClick={(e) => e.stopPropagation()}>
|
||||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList className="menu__list">
|
<MenuList className="menu__list">
|
||||||
|
|
|
@ -109,6 +109,7 @@ function Page(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<main
|
<main
|
||||||
|
id={'main-content'}
|
||||||
className={classnames(MAIN_CLASS, className, {
|
className={classnames(MAIN_CLASS, className, {
|
||||||
'main--full-width': fullWidthPage,
|
'main--full-width': fullWidthPage,
|
||||||
'main--auth-page': authPage,
|
'main--auth-page': authPage,
|
||||||
|
|
|
@ -13,11 +13,12 @@ type Props = {
|
||||||
label: ?string,
|
label: ?string,
|
||||||
reward: Reward,
|
reward: Reward,
|
||||||
button: ?boolean,
|
button: ?boolean,
|
||||||
|
disabled: boolean,
|
||||||
claimReward: (Reward) => void,
|
claimReward: (Reward) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RewardLink = (props: Props) => {
|
const RewardLink = (props: Props) => {
|
||||||
const { reward, claimReward, label, isPending, button } = props;
|
const { reward, claimReward, label, isPending, button, disabled = false } = props;
|
||||||
let displayLabel = label;
|
let displayLabel = label;
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
displayLabel = __('Claiming...');
|
displayLabel = __('Claiming...');
|
||||||
|
@ -34,7 +35,7 @@ const RewardLink = (props: Props) => {
|
||||||
return !reward ? null : (
|
return !reward ? null : (
|
||||||
<Button
|
<Button
|
||||||
button={button ? 'primary' : 'link'}
|
button={button ? 'primary' : 'link'}
|
||||||
disabled={isPending}
|
disabled={disabled || isPending}
|
||||||
label={<LbcMessage>{displayLabel}</LbcMessage>}
|
label={<LbcMessage>{displayLabel}</LbcMessage>}
|
||||||
aria-label={displayLabel}
|
aria-label={displayLabel}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -23,10 +23,11 @@ type Props = {
|
||||||
claim_code: string,
|
claim_code: string,
|
||||||
},
|
},
|
||||||
user: User,
|
user: User,
|
||||||
|
disabled: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RewardTile = (props: Props) => {
|
const RewardTile = (props: Props) => {
|
||||||
const { reward, openRewardCodeModal, openSetReferrerModal, user } = props;
|
const { reward, openRewardCodeModal, openSetReferrerModal, user, disabled = false } = props;
|
||||||
const referrerSet = user && user.invited_by_id;
|
const referrerSet = user && user.invited_by_id;
|
||||||
const claimed = !!reward.transaction_id;
|
const claimed = !!reward.transaction_id;
|
||||||
const customActionsRewards = [rewards.TYPE_REFERRAL, rewards.TYPE_REFEREE];
|
const customActionsRewards = [rewards.TYPE_REFERRAL, rewards.TYPE_REFEREE];
|
||||||
|
@ -38,18 +39,25 @@ const RewardTile = (props: Props) => {
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
{reward.reward_type === rewards.TYPE_GENERATED_CODE && (
|
{reward.reward_type === rewards.TYPE_GENERATED_CODE && (
|
||||||
<Button button="primary" onClick={openRewardCodeModal} label={__('Enter Code')} />
|
<Button button="primary" onClick={openRewardCodeModal} label={__('Enter Code')} disabled={disabled} />
|
||||||
)}
|
)}
|
||||||
{reward.reward_type === rewards.TYPE_REFERRAL && (
|
{reward.reward_type === rewards.TYPE_REFERRAL && (
|
||||||
<Button button="primary" navigate="/$/invite" label={__('Go To Invites')} />
|
<Button
|
||||||
|
button="primary"
|
||||||
|
navigate="/$/invite"
|
||||||
|
label={__('Go To Invites')}
|
||||||
|
aria-hidden={disabled}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{reward.reward_type === rewards.TYPE_REFEREE && (
|
{reward.reward_type === rewards.TYPE_REFEREE && (
|
||||||
<>
|
<>
|
||||||
{referrerSet && <RewardLink button reward_type={reward.reward_type} />}
|
{referrerSet && <RewardLink button reward_type={reward.reward_type} disabled={disabled} />}
|
||||||
<Button
|
<Button
|
||||||
button={referrerSet ? 'link' : 'primary'}
|
button={referrerSet ? 'link' : 'primary'}
|
||||||
onClick={openSetReferrerModal}
|
onClick={openSetReferrerModal}
|
||||||
label={referrerSet ? __('Change Inviter') : __('Set Inviter')}
|
label={referrerSet ? __('Change Inviter') : __('Set Inviter')}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -59,7 +67,7 @@ const RewardTile = (props: Props) => {
|
||||||
<Icon icon={ICONS.COMPLETED} /> {__('Reward claimed.')}
|
<Icon icon={ICONS.COMPLETED} /> {__('Reward claimed.')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<RewardLink button claim_code={reward.claim_code} />
|
<RewardLink button claim_code={reward.claim_code} disabled={disabled} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -331,6 +331,7 @@ function SideNavigation(props: Props) {
|
||||||
>
|
>
|
||||||
{!isOnFilePage && (
|
{!isOnFilePage && (
|
||||||
<nav
|
<nav
|
||||||
|
aria-label={'Sidebar'}
|
||||||
className={classnames('navigation', {
|
className={classnames('navigation', {
|
||||||
'navigation--micro': microNavigation,
|
'navigation--micro': microNavigation,
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
|
22
ui/component/skipNavigationButton/index.jsx
Normal file
22
ui/component/skipNavigationButton/index.jsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
// Allow screen reader users ( or keyboard navigation )
|
||||||
|
// to jump to main content
|
||||||
|
export default function SkipNavigationButton() {
|
||||||
|
const skipNavigation = (e) => {
|
||||||
|
// Match any focusable element
|
||||||
|
const focusableElementQuery = `
|
||||||
|
#main-content [tabindex]:not([tabindex="-1"]):not(:disabled),
|
||||||
|
#main-content a:not([aria-hidden]):not([tabindex="-1"]):not(:disabled),
|
||||||
|
#main-content button:not([aria-hidden]):not([tabindex="-1"]):not(:disabled)
|
||||||
|
`;
|
||||||
|
// Find first focusable element
|
||||||
|
const element = document.querySelector(focusableElementQuery);
|
||||||
|
// Trigger focus to skip navigation
|
||||||
|
if (element && element.focus) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return <Button className={'skip-button'} onClick={skipNavigation} label={__('Skip Navigation')} button={'link'} />;
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ type Props = {
|
||||||
inline: boolean,
|
inline: boolean,
|
||||||
external?: boolean,
|
external?: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
focusable: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class UriIndicator extends React.PureComponent<Props> {
|
class UriIndicator extends React.PureComponent<Props> {
|
||||||
|
@ -45,8 +46,9 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
claim,
|
claim,
|
||||||
children,
|
children,
|
||||||
inline,
|
inline,
|
||||||
hideAnonymous = false,
|
focusable = true,
|
||||||
external = false,
|
external = false,
|
||||||
|
hideAnonymous = false,
|
||||||
className,
|
className,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -86,7 +88,13 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
return (
|
return (
|
||||||
<Button className={className} target={external ? '_blank' : undefined} navigate={channelLink}>
|
<Button
|
||||||
|
aria-hidden={!focusable}
|
||||||
|
tabIndex={focusable ? 0 : -1}
|
||||||
|
className={className}
|
||||||
|
target={external ? '_blank' : undefined}
|
||||||
|
navigate={channelLink}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -96,6 +104,8 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
className={classnames(className, 'button--uri-indicator')}
|
className={classnames(className, 'button--uri-indicator')}
|
||||||
navigate={channelLink}
|
navigate={channelLink}
|
||||||
target={external ? '_blank' : undefined}
|
target={external ? '_blank' : undefined}
|
||||||
|
aria-hidden={!focusable}
|
||||||
|
tabIndex={focusable ? 0 : -1}
|
||||||
>
|
>
|
||||||
{inner}
|
{inner}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import formatMediaDuration from 'util/formatMediaDuration';
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: ?StreamClaim,
|
claim: ?StreamClaim,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
@ -9,19 +9,11 @@ type Props = {
|
||||||
function VideoDuration(props: Props) {
|
function VideoDuration(props: Props) {
|
||||||
const { claim, className } = props;
|
const { claim, className } = props;
|
||||||
|
|
||||||
const video = claim && claim.value && (claim.value.video || claim.value.audio);
|
const media = claim && claim.value && (claim.value.video || claim.value.audio);
|
||||||
let duration;
|
let duration;
|
||||||
if (video && video.duration) {
|
if (media && media.duration) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
let date = new Date(null);
|
duration = formatMediaDuration(media.duration);
|
||||||
date.setSeconds(video.duration);
|
|
||||||
let timeString = date.toISOString().substr(11, 8);
|
|
||||||
|
|
||||||
if (timeString.startsWith('00:')) {
|
|
||||||
timeString = timeString.substr(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
duration = timeString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return duration ? <span className={className}>{duration}</span> : null;
|
return duration ? <span className={className}>{duration}</span> : null;
|
||||||
|
|
|
@ -14,8 +14,8 @@ type ModalProps = {
|
||||||
abortButtonLabel?: string,
|
abortButtonLabel?: string,
|
||||||
confirmButtonDisabled?: boolean,
|
confirmButtonDisabled?: boolean,
|
||||||
abortButtonDisabled?: boolean,
|
abortButtonDisabled?: boolean,
|
||||||
onConfirmed?: any => any,
|
onConfirmed?: (any) => any,
|
||||||
onAborted?: any => any,
|
onAborted?: (any) => any,
|
||||||
className?: string,
|
className?: string,
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
extraContent?: React.Node,
|
extraContent?: React.Node,
|
||||||
|
@ -52,7 +52,13 @@ export function Modal(props: ModalProps) {
|
||||||
>
|
>
|
||||||
{title && <h1 className="card__title card__title--deprecated">{title}</h1>}
|
{title && <h1 className="card__title card__title--deprecated">{title}</h1>}
|
||||||
{type === 'card' && (
|
{type === 'card' && (
|
||||||
<Button iconSize={isMobile ? 24 : undefined} button="close" icon={ICONS.REMOVE} onClick={onAborted} />
|
<Button
|
||||||
|
iconSize={isMobile ? 24 : undefined}
|
||||||
|
button="close"
|
||||||
|
aria-label={__('Close')}
|
||||||
|
icon={ICONS.REMOVE}
|
||||||
|
onClick={onAborted}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
{type === 'custom' || type === 'card' ? null : ( // custom modals define their own buttons
|
{type === 'custom' || type === 'card' ? null : ( // custom modals define their own buttons
|
||||||
|
|
|
@ -24,6 +24,7 @@ import ClaimMenuList from 'component/claimMenuList';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
import TruncatedText from 'component/common/truncated-text';
|
||||||
// $FlowFixMe cannot resolve ...
|
// $FlowFixMe cannot resolve ...
|
||||||
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
||||||
|
|
||||||
|
@ -229,7 +230,9 @@ function ChannelPage(props: Props) {
|
||||||
<div className="channel__primary-info">
|
<div className="channel__primary-info">
|
||||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
||||||
<h1 className="channel__title">
|
<h1 className="channel__title">
|
||||||
|
<TruncatedText lines={2} showTooltip>
|
||||||
{title || '@' + channelName}
|
{title || '@' + channelName}
|
||||||
|
</TruncatedText>
|
||||||
<ChannelStakedIndicator uri={uri} large />
|
<ChannelStakedIndicator uri={uri} large />
|
||||||
</h1>
|
</h1>
|
||||||
<div className="channel__meta">
|
<div className="channel__meta">
|
||||||
|
|
|
@ -102,6 +102,8 @@ class RewardsPage extends PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCustomRewardCode() {
|
renderCustomRewardCode() {
|
||||||
|
const { user } = this.props;
|
||||||
|
const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
|
||||||
return (
|
return (
|
||||||
<RewardTile
|
<RewardTile
|
||||||
key={REWARD_TYPES.TYPE_GENERATED_CODE}
|
key={REWARD_TYPES.TYPE_GENERATED_CODE}
|
||||||
|
@ -110,6 +112,7 @@ class RewardsPage extends PureComponent<Props> {
|
||||||
reward_title: __('Custom Code'),
|
reward_title: __('Custom Code'),
|
||||||
reward_description: __('Are you a supermodel or rockstar that received a custom reward code? Claim it here.'),
|
reward_description: __('Are you a supermodel or rockstar that received a custom reward code? Claim it here.'),
|
||||||
}}
|
}}
|
||||||
|
disabled={isNotEligible}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -155,12 +158,13 @@ class RewardsPage extends PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
aria-hidden={isNotEligible}
|
||||||
className={classnames('card__list', {
|
className={classnames('card__list', {
|
||||||
'card--disabled': isNotEligible,
|
'card--disabled': isNotEligible,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{rewards.map((reward) => (
|
{rewards.map((reward) => (
|
||||||
<RewardTile key={reward.claim_code} reward={reward} />
|
<RewardTile disabled={isNotEligible} key={reward.claim_code} reward={reward} />
|
||||||
))}
|
))}
|
||||||
{this.renderCustomRewardCode()}
|
{this.renderCustomRewardCode()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -236,6 +236,7 @@
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
background-color: var(--color-button-alt-bg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
$cover-z-index: 0;
|
$cover-z-index: 0;
|
||||||
$metadata-z-index: 1;
|
$metadata-z-index: 1;
|
||||||
|
$actions-z-index: 2;
|
||||||
|
|
||||||
.channel-cover {
|
.channel-cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -200,7 +201,7 @@ $metadata-z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: var(--spacing-m);
|
right: var(--spacing-m);
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
z-index: $metadata-z-index;
|
z-index: $actions-z-index;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: var(--font-base);
|
font-size: var(--font-base);
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.claim__menu-button {
|
.claim__menu-button {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,7 +269,7 @@
|
||||||
.claim-preview__title {
|
.claim-preview__title {
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: var(--font-body);
|
font-size: var(--font-body);
|
||||||
margin-right: auto;
|
margin-right: var(--spacing-l);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -475,7 +475,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.claim__menu-button {
|
.claim__menu-button {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.collection-preview__overlay-thumbs {
|
.collection-preview__overlay-thumbs {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -531,7 +531,7 @@
|
||||||
.claim-tile__title {
|
.claim-tile__title {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
padding-right: var(--spacing-l);
|
padding-right: var(--spacing-xl);
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-bottom: var(--spacing-s);
|
margin-bottom: var(--spacing-s);
|
||||||
|
|
||||||
|
@ -540,14 +540,8 @@
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
|
|
||||||
.claim__menu-button {
|
|
||||||
right: 0.2rem;
|
|
||||||
top: var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (min-width: $breakpoint-small) {
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
padding-right: var(--spacing-m);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -745,29 +739,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim__menu-button {
|
.claim-tile__header {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: var(--spacing-xs);
|
|
||||||
right: var(--spacing-xs);
|
|
||||||
|
|
||||||
.icon {
|
.claim__menu-button {
|
||||||
stroke: var(--color-text);
|
right: 0.2rem;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: $breakpoint-small) {
|
|
||||||
&:not([aria-expanded='true']) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim__menu-button--inline {
|
.menu__button {
|
||||||
|
&.claim__menu-button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--spacing-xs);
|
||||||
|
right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.claim__menu-button--inline {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
|
||||||
right: auto;
|
|
||||||
top: auto;
|
|
||||||
@extend .button--alt;
|
@extend .button--alt;
|
||||||
padding: 0 var(--spacing-xxs);
|
width: var(--height-button);
|
||||||
|
padding: 0;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) {
|
||||||
|
.claim-preview--tile:not(:hover),
|
||||||
|
.claim-preview__wrapper:not(:hover) {
|
||||||
|
.claim__menu-button:not(:focus):not([aria-expanded='true']) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-preview__overlay-properties {
|
.claim-preview__overlay-properties {
|
||||||
|
|
|
@ -263,14 +263,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
.comment__menu {
|
.comment__menu {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
@include linkFocus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__char-count {
|
.comment__char-count {
|
||||||
|
@ -295,14 +287,6 @@ $thumbnailWidthSmall: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__menu-icon--hovering {
|
|
||||||
stroke: var(--color-comment-menu-hovering);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__menu-icon {
|
|
||||||
stroke: var(--color-comment-menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment__menu-list {
|
.comment__menu-list {
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
border-radius: var(--card-radius);
|
border-radius: var(--card-radius);
|
||||||
|
|
|
@ -9,6 +9,25 @@
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
|
|
||||||
|
.skip-button {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: var(--spacing-l);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
opacity: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: unset;
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -318,7 +318,7 @@ $discussion-header__height: 3rem;
|
||||||
padding-bottom: var(--spacing-xxs);
|
padding-bottom: var(--spacing-xxs);
|
||||||
.markdown-preview {
|
.markdown-preview {
|
||||||
p {
|
p {
|
||||||
word-break: break-all;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,14 +218,6 @@ $contentMaxWidth: 60rem;
|
||||||
.icon {
|
.icon {
|
||||||
stroke: var(--color-text-help);
|
stroke: var(--color-text-help);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
@include linkFocus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__filter {
|
.notification__filter {
|
||||||
|
|
|
@ -44,10 +44,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__button {
|
.menu__button {
|
||||||
&:hover {
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: 0.3rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
border-radius: var(--border-radius);
|
stroke: var(--color-menu);
|
||||||
background-color: var(--color-card-background-highlighted);
|
}
|
||||||
|
|
||||||
|
.comment__menu-icon--hovering {
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--color-button-alt-bg);
|
||||||
|
.icon {
|
||||||
|
stroke: var(--color-menu-hovering);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,10 +124,10 @@
|
||||||
--color-menu-background: var(--color-header-background);
|
--color-menu-background: var(--color-header-background);
|
||||||
--color-menu-background--active: var(--color-card-background-highlighted);
|
--color-menu-background--active: var(--color-card-background-highlighted);
|
||||||
--color-menu-icon: var(--color-navigation-link);
|
--color-menu-icon: var(--color-navigation-link);
|
||||||
|
--color-menu: var(--color-gray-3);
|
||||||
|
--color-menu-hovering: var(--color-gray-6);
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
--color-comment-menu: var(--color-gray-3);
|
|
||||||
--color-comment-menu-hovering: var(--color-gray-6);
|
|
||||||
--color-comment-highlighted: #fff2d9;
|
--color-comment-highlighted: #fff2d9;
|
||||||
--color-comment-threadline: var(--color-gray-1);
|
--color-comment-threadline: var(--color-gray-1);
|
||||||
--color-comment-threadline-hover: var(--color-gray-4);
|
--color-comment-threadline-hover: var(--color-gray-4);
|
||||||
|
|
|
@ -90,10 +90,10 @@
|
||||||
--color-menu-background: var(--color-header-background);
|
--color-menu-background: var(--color-header-background);
|
||||||
--color-menu-background--active: var(--color-gray-7);
|
--color-menu-background--active: var(--color-gray-7);
|
||||||
--color-menu-icon: var(--color-gray-4);
|
--color-menu-icon: var(--color-gray-4);
|
||||||
|
--color-menu: var(--color-gray-5);
|
||||||
|
--color-menu-hovering: var(--color-gray-2);
|
||||||
|
|
||||||
// Comments
|
// Comments
|
||||||
--color-comment-menu: var(--color-gray-5);
|
|
||||||
--color-comment-menu-hovering: var(--color-gray-2);
|
|
||||||
--color-comment-threadline: #434b54;
|
--color-comment-threadline: #434b54;
|
||||||
--color-comment-threadline-hover: var(--color-gray-4);
|
--color-comment-threadline-hover: var(--color-gray-4);
|
||||||
--color-comment-highlighted: #484734;
|
--color-comment-highlighted: #484734;
|
||||||
|
|
32
ui/util/formatAriaLabel.js
Normal file
32
ui/util/formatAriaLabel.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import DateTime from 'component/dateTime';
|
||||||
|
|
||||||
|
export function formatClaimPreviewTitle(title, channelTitle, date, mediaDuration) {
|
||||||
|
// Aria-label value for claim preview
|
||||||
|
let ariaDate = date ? DateTime.getTimeAgoStr(date) : null;
|
||||||
|
let ariaLabelData = title;
|
||||||
|
|
||||||
|
if (mediaDuration) {
|
||||||
|
if (ariaDate) {
|
||||||
|
ariaLabelData = __('%title% by %channelTitle% %ariaDate%, %mediaDuration%', {
|
||||||
|
title,
|
||||||
|
channelTitle,
|
||||||
|
ariaDate,
|
||||||
|
mediaDuration,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ariaLabelData = __('%title% by %channelTitle%, %mediaDuration%', {
|
||||||
|
title,
|
||||||
|
channelTitle,
|
||||||
|
mediaDuration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ariaDate) {
|
||||||
|
ariaLabelData = __('%title% by %channelTitle% %ariaDate%', { title, channelTitle, ariaDate });
|
||||||
|
} else {
|
||||||
|
ariaLabelData = __('%title% by %channelTitle%', { title, channelTitle });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ariaLabelData;
|
||||||
|
}
|
24
ui/util/formatMediaDuration.js
Normal file
24
ui/util/formatMediaDuration.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default function formatMediaDuration(duration = 0, config) {
|
||||||
|
const options = {
|
||||||
|
screenReader: false,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimize for screen readers
|
||||||
|
if (options.screenReader) {
|
||||||
|
return moment.utc(moment.duration(duration, 'seconds').asMilliseconds()).format('HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal format
|
||||||
|
let date = new Date(null);
|
||||||
|
date.setSeconds(duration);
|
||||||
|
|
||||||
|
let timeString = date.toISOString().substr(11, 8);
|
||||||
|
if (timeString.startsWith('00:')) {
|
||||||
|
timeString = timeString.substr(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString;
|
||||||
|
}
|
|
@ -13544,8 +13544,9 @@ react@^15.6.1:
|
||||||
prop-types "^15.5.10"
|
prop-types "^15.5.10"
|
||||||
|
|
||||||
react@^16.8.2:
|
react@^16.8.2:
|
||||||
version "16.13.0"
|
version "16.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||||
|
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
Loading…
Reference in a new issue