#6470 Improve accessibility and some minor css fixes

This commit is contained in:
infinite-persistence 2021-07-30 09:32:36 +08:00
commit 0cdf881941
No known key found for this signature in database
GPG key ID: B9C3252EDC3D0AA0
37 changed files with 385 additions and 168 deletions

View file

@ -2021,14 +2021,14 @@
"Chat": "Chat", "Chat": "Chat",
"Tipped": "Tipped", "Tipped": "Tipped",
"Fromage": "Fromage", "Fromage": "Fromage",
"In Favorites": "In Favorites", "In Favorites": "In Favorites",
"In Watch Later": "In Watch Later", "In Watch Later": "In Watch Later",
"In %lastCollectionName%": "In %lastCollectionName%", "In %lastCollectionName%": "In %lastCollectionName%",
"Remove from Watch Later": "Remove from Watch Later", "Remove from Watch Later": "Remove from Watch Later",
"Add to Watch Later": "Add to Watch Later", "Add to Watch Later": "Add to Watch Later",
"Added": "Added", "Added": "Added",
"Item added to Watch Later": "Item added to Watch Later", "Item added to Watch Later": "Item added to Watch Later",
"Item removed from Watch Later": "Item removed from Watch Later", "Item removed from Watch Later": "Item removed from Watch Later",
"Item added to %lastCollectionName%": "Item added to %lastCollectionName%", "Item added to %lastCollectionName%": "Item added to %lastCollectionName%",
"Item removed from %lastCollectionName%": "Item removed from %lastCollectionName%", "Item removed from %lastCollectionName%": "Item removed from %lastCollectionName%",
"Your publish is being confirmed and will be live soon": "Your publish is being confirmed and will be live soon", "Your publish is being confirmed and will be live soon": "Your publish is being confirmed and will be live soon",
@ -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--"
} }

View file

@ -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);

View file

@ -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) {

View file

@ -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,32 +24,43 @@ 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';
pending: props.uri && makeSelectClaimIsPending(props.uri)(state), import formatMediaDuration from 'util/formatMediaDuration';
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state), const select = (state, props) => {
obscureNsfw: selectShowMatureContent(state) === false, const claim = props.uri && makeSelectClaimForUri(props.uri)(state);
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state), const media = claim && claim.value && (claim.value.video || claim.value.audio);
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true });
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state), return {
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state), claim,
blackListedOutpoints: selectBlackListedOutpoints(state), mediaDuration,
filteredOutpoints: selectFilteredOutpoints(state), date: props.uri && makeSelectDateForUri(props.uri)(state),
mutedUris: selectMutedChannels(state), title: props.uri && makeSelectTitleForUri(props.uri)(state),
blockedUris: selectModerationBlockList(state), pending: props.uri && makeSelectClaimIsPending(props.uri)(state),
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state), reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state), obscureNsfw: selectShowMatureContent(state) === false,
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state), claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state), isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state),
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state), nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
collectionUris: makeSelectUrlsForCollectionId(props.collectionId)(state), blackListedOutpoints: selectBlackListedOutpoints(state),
collectionIndex: makeSelectIndexForUrlInCollection(props.uri, props.collectionId)(state), filteredOutpoints: selectFilteredOutpoints(state),
}); mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
isCollectionMine: makeSelectCollectionIsMine(props.collectionId)(state),
collectionUris: makeSelectUrlsForCollectionId(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)),

View file

@ -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>
); );

View file

@ -9,25 +9,35 @@ 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);
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state), const media = claim && claim.value && (claim.value.video || claim.value.audio);
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), const mediaDuration = media && media.duration && formatMediaDuration(media.duration, { screenReader: true });
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
title: props.uri && makeSelectTitleForUri(props.uri)(state), return {
blackListedOutpoints: selectBlackListedOutpoints(state), claim,
filteredOutpoints: selectFilteredOutpoints(state), mediaDuration,
blockedChannelUris: selectMutedChannels(state), date: props.uri && makeSelectDateForUri(props.uri)(state),
showMature: selectShowMatureContent(state), channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state), thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
}); title: props.uri && makeSelectTitleForUri(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectMutedChannels(state),
showMature: selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
};
};
const perform = (dispatch) => ({ const perform = (dispatch) => ({
resolveUri: (uri) => dispatch(doResolveUri(uri)), resolveUri: (uri) => dispatch(doResolveUri(uri)),

View file

@ -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,17 +236,19 @@ function ClaimPreviewTile(props: Props) {
)} )}
</FileThumbnail> </FileThumbnail>
</NavLink> </NavLink>
<NavLink {...navLinkProps}> <div className="claim-tile__header">
<h2 className="claim-tile__title"> <NavLink aria-label={ariaLabelData} {...navLinkProps}>
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} /> <h2 className="claim-tile__title">
{isChannel && ( <TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
<div className="claim-tile__about"> {isChannel && (
<UriIndicator uri={uri} /> <div className="claim-tile__about">
</div> <UriIndicator uri={uri} />
)} </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>

View file

@ -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>

View file

@ -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}
/> />
); );

View file

@ -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}
/> />
); );
} }

View file

@ -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(
updatePublishForm({ filePath: selectedFile }); (selectedFile) => {
hideDropArea(); updatePublishForm({ filePath: selectedFile });
}, [updatePublishForm, hideDropArea]); 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>

View file

@ -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}
/> />
); );
} }

View file

@ -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();

View file

@ -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">

View file

@ -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,

View file

@ -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={() => {

View file

@ -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>
} }

View file

@ -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'

View 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'} />;
}

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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">
{title || '@' + channelName} <TruncatedText lines={2} showTooltip>
{title || '@' + channelName}
</TruncatedText>
<ChannelStakedIndicator uri={uri} large /> <ChannelStakedIndicator uri={uri} large />
</h1> </h1>
<div className="channel__meta"> <div className="channel__meta">

View file

@ -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>

View file

@ -236,6 +236,7 @@
&:focus { &:focus {
box-shadow: none; box-shadow: none;
background-color: var(--color-button-alt-bg);
} }
} }

View file

@ -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);

View file

@ -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 {
position: relative; &.claim__menu-button {
display: block; position: absolute;
right: auto; top: var(--spacing-xs);
top: auto; right: var(--spacing-xs);
@extend .button--alt; }
padding: 0 var(--spacing-xxs);
&.claim__menu-button--inline {
position: relative;
@extend .button--alt;
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 {

View file

@ -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);

View file

@ -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;
} }

View file

@ -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;
} }
} }
} }

View file

@ -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 {

View file

@ -44,10 +44,25 @@
} }
.menu__button { .menu__button {
display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
padding: 0.3rem;
.icon {
stroke: var(--color-menu);
}
.comment__menu-icon--hovering {
}
&:focus,
&:hover { &:hover {
opacity: 1;
background-color: var(--color-button-alt-bg);
.icon { .icon {
border-radius: var(--border-radius); stroke: var(--color-menu-hovering);
background-color: var(--color-card-background-highlighted);
} }
} }
} }

View file

@ -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);

View file

@ -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;

View 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;
}

View 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;
}

View file

@ -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"