#6470 Improve accessibility and some minor css fixes
This commit is contained in:
commit
0cdf881941
37 changed files with 385 additions and 168 deletions
|
@ -2021,14 +2021,14 @@
|
|||
"Chat": "Chat",
|
||||
"Tipped": "Tipped",
|
||||
"Fromage": "Fromage",
|
||||
"In Favorites": "In Favorites",
|
||||
"In Favorites": "In Favorites",
|
||||
"In Watch Later": "In Watch Later",
|
||||
"In %lastCollectionName%": "In %lastCollectionName%",
|
||||
"Remove from Watch Later": "Remove from 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 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 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",
|
||||
|
@ -2057,7 +2057,16 @@
|
|||
"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",
|
||||
"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 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--"
|
||||
}
|
||||
|
|
|
@ -285,6 +285,11 @@ function App(props: Props) {
|
|||
}
|
||||
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
document.documentElement.setAttribute('lang', language);
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!languages.includes(language)) {
|
||||
setLanguage(language);
|
||||
|
|
|
@ -97,6 +97,9 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
const combinedRef = useCombinedRefs(ref, innerRef, myref);
|
||||
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 = (
|
||||
<span className="button__content">
|
||||
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
||||
|
@ -150,6 +153,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
className={combinedClassName}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
|
@ -196,6 +200,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
disabled={disable}
|
||||
className={combinedClassName}
|
||||
activeClassName={activeClass}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{content}
|
||||
</NavLink>
|
||||
|
@ -216,6 +221,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
}}
|
||||
className={combinedClassName}
|
||||
activeClassName={activeClass}
|
||||
aria-label={ariaLabel}
|
||||
{...otherProps}
|
||||
>
|
||||
{content}
|
||||
|
@ -224,7 +230,7 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
<button
|
||||
ref={combinedRef}
|
||||
title={title || defaultTooltip}
|
||||
aria-label={description || label || title}
|
||||
aria-label={ariaLabel}
|
||||
className={combinedClassName}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
|
|
|
@ -15,6 +15,8 @@ import {
|
|||
doCollectionEdit,
|
||||
makeSelectUrlsForCollectionId,
|
||||
makeSelectIndexForUrlInCollection,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectDateForUri,
|
||||
} from 'lbry-redux';
|
||||
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
|
@ -22,32 +24,43 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
|||
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
import ClaimPreview from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: props.uri && makeSelectClaimIsPending(props.uri)(state),
|
||||
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
||||
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
|
||||
obscureNsfw: selectShowMatureContent(state) === false,
|
||||
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
|
||||
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
|
||||
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
|
||||
repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
||||
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(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),
|
||||
});
|
||||
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),
|
||||
reflectingProgress: props.uri && makeSelectReflectingClaimForUri(props.uri)(state),
|
||||
obscureNsfw: selectShowMatureContent(state) === false,
|
||||
claimIsMine: props.uri && makeSelectClaimIsMine(props.uri)(state),
|
||||
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
|
||||
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
|
||||
repostClaim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
||||
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(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) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
import type { Node } from 'react';
|
||||
import React, { useEffect, forwardRef } from 'react';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
import { isEmpty } from 'util/object';
|
||||
import { lazyImport } from 'util/lazyImport';
|
||||
import classnames from 'classnames';
|
||||
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { isEmpty } from 'util/object';
|
||||
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||
import FileThumbnail from 'component/fileThumbnail';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||
|
@ -85,6 +86,8 @@ type Props = {
|
|||
collectionUris: Array<Collection>,
|
||||
collectionIndex?: number,
|
||||
disableNavigation?: boolean,
|
||||
mediaDuration?: string,
|
||||
date?: any,
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -99,8 +102,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
// claim properties
|
||||
// is the claim consider nsfw?
|
||||
nsfw,
|
||||
date,
|
||||
title,
|
||||
claimIsMine,
|
||||
streamingUrl,
|
||||
mediaDuration,
|
||||
// user properties
|
||||
channelIsBlocked,
|
||||
hasVisitedUri,
|
||||
|
@ -175,6 +181,21 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
|
||||
const isChannelUri = isValid ? parseURI(uri).isChannel : false;
|
||||
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 || '/');
|
||||
if (listId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
|
@ -313,18 +334,18 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
})}
|
||||
>
|
||||
{isChannelUri && claim ? (
|
||||
<UriIndicator uri={uri} link>
|
||||
<UriIndicator focusable={false} uri={uri} link>
|
||||
<ChannelThumbnail uri={uri} small={type === 'inline'} />
|
||||
</UriIndicator>
|
||||
) : (
|
||||
<>
|
||||
{!pending ? (
|
||||
<NavLink {...navLinkProps}>
|
||||
<NavLink aria-hidden tabIndex={-1} {...navLinkProps}>
|
||||
<FileThumbnail thumbnail={thumbnailUrl}>
|
||||
{/* @if TARGET='app' */}
|
||||
{claim && !isCollection && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton hideDownloadStatus />
|
||||
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton hideDownloadStatus />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
@ -335,7 +356,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
{isPlayable && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileWatchLaterLink uri={uri} />
|
||||
<FileWatchLaterLink focusable={false} uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
</FileThumbnail>
|
||||
|
@ -352,7 +373,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
{pending ? (
|
||||
<ClaimPreviewTitle uri={uri} />
|
||||
) : (
|
||||
<NavLink aria-current={active && 'page'} {...navLinkProps}>
|
||||
<NavLink aria-label={ariaLabelData} aria-current={active ? 'page' : null} {...navLinkProps}>
|
||||
<ClaimPreviewTitle uri={uri} />
|
||||
</NavLink>
|
||||
)}
|
||||
|
@ -451,9 +472,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!hideMenu && (
|
||||
<ClaimMenuList uri={uri} collectionId={listId} />
|
||||
)}
|
||||
{!hideMenu && <ClaimMenuList uri={uri} collectionId={listId} />}
|
||||
</>
|
||||
</WrapperElement>
|
||||
);
|
||||
|
|
|
@ -9,25 +9,35 @@ import {
|
|||
makeSelectChannelForClaimUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
makeSelectClaimIsStreamPlaceholder,
|
||||
makeSelectDateForUri,
|
||||
} from 'lbry-redux';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import ClaimPreviewTile from './view';
|
||||
import formatMediaDuration from 'util/formatMediaDuration';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: props.uri && makeSelectClaimForUri(props.uri)(state),
|
||||
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
|
||||
isResolvingUri: props.uri && makeSelectIsUriResolving(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 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),
|
||||
channel: props.uri && makeSelectChannelForClaimUri(props.uri)(state),
|
||||
isResolvingUri: props.uri && makeSelectIsUriResolving(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) => ({
|
||||
resolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||
|
|
|
@ -10,6 +10,7 @@ import ChannelThumbnail from 'component/channelThumbnail';
|
|||
import SubscribeButton from 'component/subscribeButton';
|
||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
|
||||
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
|
@ -22,7 +23,9 @@ import PlaceholderTx from 'static/img/placeholderTx.gif';
|
|||
|
||||
type Props = {
|
||||
uri: string,
|
||||
date?: any,
|
||||
claim: ?Claim,
|
||||
mediaDuration?: string,
|
||||
resolveUri: (string) => void,
|
||||
isResolvingUri: boolean,
|
||||
history: { push: (string) => void },
|
||||
|
@ -54,6 +57,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
const {
|
||||
history,
|
||||
uri,
|
||||
date,
|
||||
isResolvingUri,
|
||||
thumbnail,
|
||||
title,
|
||||
|
@ -73,6 +77,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
showNoSourceClaims,
|
||||
isLivestream,
|
||||
collectionId,
|
||||
mediaDuration,
|
||||
} = props;
|
||||
const isRepost = claim && claim.repost_channel_url;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
|
@ -115,6 +120,10 @@ function ClaimPreviewTile(props: Props) {
|
|||
const signingChannel = claim && claim.signing_channel;
|
||||
const isChannel = claim && claim.value_type === 'channel';
|
||||
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) {
|
||||
if (navigateUrl) {
|
||||
|
@ -189,28 +198,27 @@ function ClaimPreviewTile(props: Props) {
|
|||
|
||||
return (
|
||||
<li
|
||||
role="link"
|
||||
onClick={handleClick}
|
||||
className={classnames('card claim-preview--tile', {
|
||||
'claim-preview__wrapper--channel': isChannel,
|
||||
'claim-preview__live': live,
|
||||
})}
|
||||
>
|
||||
<NavLink {...navLinkProps}>
|
||||
<NavLink {...navLinkProps} role="none" tabIndex={-1} aria-hidden>
|
||||
<FileThumbnail thumbnail={thumbnailUrl} allowGifs>
|
||||
{!isChannel && (
|
||||
<React.Fragment>
|
||||
{/* @if TARGET='app' */}
|
||||
{isStream && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileDownloadLink uri={canonicalUrl} hideOpenButton />
|
||||
<FileDownloadLink focusable={false} uri={canonicalUrl} hideOpenButton />
|
||||
</div>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
{isPlayable && (
|
||||
<div className="claim-preview__hover-actions">
|
||||
<FileWatchLaterLink uri={uri} />
|
||||
<FileWatchLaterLink focusable={false} uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -228,17 +236,19 @@ function ClaimPreviewTile(props: Props) {
|
|||
)}
|
||||
</FileThumbnail>
|
||||
</NavLink>
|
||||
<NavLink {...navLinkProps}>
|
||||
<h2 className="claim-tile__title">
|
||||
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
|
||||
{isChannel && (
|
||||
<div className="claim-tile__about">
|
||||
<UriIndicator uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
<ClaimMenuList uri={uri} collectionId={listId} />
|
||||
</h2>
|
||||
</NavLink>
|
||||
<div className="claim-tile__header">
|
||||
<NavLink aria-label={ariaLabelData} {...navLinkProps}>
|
||||
<h2 className="claim-tile__title">
|
||||
<TruncatedText text={title || (claim && claim.name)} lines={isChannel ? 1 : 2} />
|
||||
{isChannel && (
|
||||
<div className="claim-tile__about">
|
||||
<UriIndicator uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
</NavLink>
|
||||
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="claim-tile__info">
|
||||
{isChannel ? (
|
||||
|
@ -247,7 +257,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<UriIndicator uri={uri} link hideAnonymous>
|
||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||
<ChannelThumbnail uri={channelUri} xsmall />
|
||||
</UriIndicator>
|
||||
|
||||
|
|
|
@ -44,7 +44,15 @@ const buildIcon = (iconStrokes: React$Node, customSvgValues = {}) =>
|
|||
export const icons = {
|
||||
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
||||
[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="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>
|
||||
|
|
|
@ -73,6 +73,7 @@ class IconComponent extends React.PureComponent<Props> {
|
|||
size={size || (sectionIcon ? 20 : 16)}
|
||||
className={classnames(`icon icon--${icon}`, className, { 'color-override': iconColor })}
|
||||
color={color}
|
||||
aria-hidden
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
downloading: boolean,
|
||||
loading: boolean,
|
||||
focusable: boolean,
|
||||
fileInfo: ?FileListItem,
|
||||
openModal: (id: string, { path: string }) => void,
|
||||
pause: () => void,
|
||||
|
@ -35,6 +36,7 @@ function FileDownloadLink(props: Props) {
|
|||
uri,
|
||||
claim,
|
||||
buttonType,
|
||||
focusable = true,
|
||||
showLabel = false,
|
||||
hideOpenButton = false,
|
||||
hideDownloadStatus = false,
|
||||
|
@ -91,6 +93,8 @@ function FileDownloadLink(props: Props) {
|
|||
pause();
|
||||
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}
|
||||
label={showLabel ? label : null}
|
||||
onClick={handleDownload}
|
||||
aria-hidden={!focusable}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ type Props = {
|
|||
index: number,
|
||||
length: number,
|
||||
location: { pathname: string },
|
||||
push: string => void,
|
||||
push: (string) => void,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ function FileDrop(props: Props) {
|
|||
const navigationTimer = React.useRef(null);
|
||||
|
||||
// Gets respective icon given a mimetype
|
||||
const getFileIcon = type => {
|
||||
const getFileIcon = (type) => {
|
||||
// Not all files have a type
|
||||
if (!type) return ICONS.FILE;
|
||||
// Detect common types
|
||||
|
@ -77,10 +77,13 @@ function FileDrop(props: Props) {
|
|||
}, [navigateToPublish]);
|
||||
|
||||
// Handle file selection
|
||||
const handleFileSelected = React.useCallback((selectedFile) => {
|
||||
updatePublishForm({ filePath: selectedFile });
|
||||
hideDropArea();
|
||||
}, [updatePublishForm, hideDropArea]);
|
||||
const handleFileSelected = React.useCallback(
|
||||
(selectedFile) => {
|
||||
updatePublishForm({ filePath: selectedFile });
|
||||
hideDropArea();
|
||||
},
|
||||
[updatePublishForm, hideDropArea]
|
||||
);
|
||||
|
||||
// Clear timers when unmounted
|
||||
React.useEffect(() => {
|
||||
|
@ -114,12 +117,12 @@ function FileDrop(props: Props) {
|
|||
React.useEffect(() => {
|
||||
if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) {
|
||||
getTree(dropData)
|
||||
.then(entries => {
|
||||
.then((entries) => {
|
||||
if (entries && entries.length) {
|
||||
setFiles(entries);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
setError(error || true);
|
||||
});
|
||||
}
|
||||
|
@ -146,7 +149,7 @@ function FileDrop(props: Props) {
|
|||
const show = files.length === 1 || (!target && drag && (!modal || modal.id !== MODALS.FILE_SELECTION));
|
||||
|
||||
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')}>
|
||||
<Icon size={64} icon={icon} className={'main-icon'} />
|
||||
<p>{target ? target.name : __(`Drop here to publish!`)} </p>
|
||||
|
|
|
@ -8,13 +8,14 @@ import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
|||
type Props = {
|
||||
uri: string,
|
||||
claim: StreamClaim,
|
||||
focusable: boolean,
|
||||
hasClaimInWatchLater: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
doCollectionEdit: (string, any) => void,
|
||||
};
|
||||
|
||||
function FileWatchLaterLink(props: Props) {
|
||||
const { claim, hasClaimInWatchLater, doToast, doCollectionEdit } = props;
|
||||
const { claim, hasClaimInWatchLater, doToast, doCollectionEdit, focusable = true } = props;
|
||||
const buttonRef = useRef();
|
||||
let isHovering = useHover(buttonRef);
|
||||
|
||||
|
@ -51,6 +52,8 @@ function FileWatchLaterLink(props: Props) {
|
|||
(isHovering ? ICONS.COMPLETED : ICONS.TIME)
|
||||
}
|
||||
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 NotificationHeaderButton from 'component/notificationHeaderButton';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import SkipNavigationButton from 'component/skipNavigationButton';
|
||||
import Logo from 'component/logo';
|
||||
// @if TARGET='app'
|
||||
import { remote } from 'electron';
|
||||
|
@ -235,6 +236,7 @@ const Header = (props: Props) => {
|
|||
) : (
|
||||
<>
|
||||
<div className="header__navigation">
|
||||
<SkipNavigationButton />
|
||||
{!authHeader && (
|
||||
<span style={{ position: 'relative' }}>
|
||||
<Button
|
||||
|
@ -245,6 +247,7 @@ const Header = (props: Props) => {
|
|||
}
|
||||
className="header__navigation-item menu__title header__navigation-item--icon"
|
||||
icon={ICONS.MENU}
|
||||
aria-expanded={sidebarOpen}
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
>
|
||||
{isAbsoluteSideNavHidden && isMobile && notificationsEnabled && <NotificationBubble />}
|
||||
|
@ -252,6 +255,7 @@ const Header = (props: Props) => {
|
|||
</span>
|
||||
)}
|
||||
<Button
|
||||
aria-label={__('Home')}
|
||||
className="header__navigation-item header__navigation-item--lbry"
|
||||
onClick={() => {
|
||||
if (history.location.pathname === '/') window.location.reload();
|
||||
|
|
|
@ -193,7 +193,7 @@ export default function Notification(props: Props) {
|
|||
|
||||
<div className="notification__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} />
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list">
|
||||
|
|
|
@ -109,6 +109,7 @@ function Page(props: Props) {
|
|||
/>
|
||||
)}
|
||||
<main
|
||||
id={'main-content'}
|
||||
className={classnames(MAIN_CLASS, className, {
|
||||
'main--full-width': fullWidthPage,
|
||||
'main--auth-page': authPage,
|
||||
|
|
|
@ -13,11 +13,12 @@ type Props = {
|
|||
label: ?string,
|
||||
reward: Reward,
|
||||
button: ?boolean,
|
||||
disabled: boolean,
|
||||
claimReward: (Reward) => void,
|
||||
};
|
||||
|
||||
const RewardLink = (props: Props) => {
|
||||
const { reward, claimReward, label, isPending, button } = props;
|
||||
const { reward, claimReward, label, isPending, button, disabled = false } = props;
|
||||
let displayLabel = label;
|
||||
if (isPending) {
|
||||
displayLabel = __('Claiming...');
|
||||
|
@ -34,7 +35,7 @@ const RewardLink = (props: Props) => {
|
|||
return !reward ? null : (
|
||||
<Button
|
||||
button={button ? 'primary' : 'link'}
|
||||
disabled={isPending}
|
||||
disabled={disabled || isPending}
|
||||
label={<LbcMessage>{displayLabel}</LbcMessage>}
|
||||
aria-label={displayLabel}
|
||||
onClick={() => {
|
||||
|
|
|
@ -23,10 +23,11 @@ type Props = {
|
|||
claim_code: string,
|
||||
},
|
||||
user: User,
|
||||
disabled: boolean,
|
||||
};
|
||||
|
||||
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 claimed = !!reward.transaction_id;
|
||||
const customActionsRewards = [rewards.TYPE_REFERRAL, rewards.TYPE_REFEREE];
|
||||
|
@ -38,18 +39,25 @@ const RewardTile = (props: Props) => {
|
|||
actions={
|
||||
<div className="section__actions">
|
||||
{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 && (
|
||||
<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 && (
|
||||
<>
|
||||
{referrerSet && <RewardLink button reward_type={reward.reward_type} />}
|
||||
{referrerSet && <RewardLink button reward_type={reward.reward_type} disabled={disabled} />}
|
||||
<Button
|
||||
button={referrerSet ? 'link' : 'primary'}
|
||||
onClick={openSetReferrerModal}
|
||||
label={referrerSet ? __('Change Inviter') : __('Set Inviter')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -59,7 +67,7 @@ const RewardTile = (props: Props) => {
|
|||
<Icon icon={ICONS.COMPLETED} /> {__('Reward claimed.')}
|
||||
</span>
|
||||
) : (
|
||||
<RewardLink button claim_code={reward.claim_code} />
|
||||
<RewardLink button claim_code={reward.claim_code} disabled={disabled} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -331,6 +331,7 @@ function SideNavigation(props: Props) {
|
|||
>
|
||||
{!isOnFilePage && (
|
||||
<nav
|
||||
aria-label={'Sidebar'}
|
||||
className={classnames('navigation', {
|
||||
'navigation--micro': microNavigation,
|
||||
// @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,
|
||||
external?: boolean,
|
||||
className?: string,
|
||||
focusable: boolean,
|
||||
};
|
||||
|
||||
class UriIndicator extends React.PureComponent<Props> {
|
||||
|
@ -45,8 +46,9 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
claim,
|
||||
children,
|
||||
inline,
|
||||
hideAnonymous = false,
|
||||
focusable = true,
|
||||
external = false,
|
||||
hideAnonymous = false,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
|
@ -86,7 +88,13 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
|
||||
if (children) {
|
||||
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}
|
||||
</Button>
|
||||
);
|
||||
|
@ -96,6 +104,8 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
className={classnames(className, 'button--uri-indicator')}
|
||||
navigate={channelLink}
|
||||
target={external ? '_blank' : undefined}
|
||||
aria-hidden={!focusable}
|
||||
tabIndex={focusable ? 0 : -1}
|
||||
>
|
||||
{inner}
|
||||
</Button>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
import formatMediaDuration from 'util/formatMediaDuration';
|
||||
type Props = {
|
||||
claim: ?StreamClaim,
|
||||
className?: string,
|
||||
|
@ -9,19 +9,11 @@ type Props = {
|
|||
function VideoDuration(props: 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;
|
||||
if (video && video.duration) {
|
||||
if (media && media.duration) {
|
||||
// $FlowFixMe
|
||||
let date = new Date(null);
|
||||
date.setSeconds(video.duration);
|
||||
let timeString = date.toISOString().substr(11, 8);
|
||||
|
||||
if (timeString.startsWith('00:')) {
|
||||
timeString = timeString.substr(3);
|
||||
}
|
||||
|
||||
duration = timeString;
|
||||
duration = formatMediaDuration(media.duration);
|
||||
}
|
||||
|
||||
return duration ? <span className={className}>{duration}</span> : null;
|
||||
|
|
|
@ -14,8 +14,8 @@ type ModalProps = {
|
|||
abortButtonLabel?: string,
|
||||
confirmButtonDisabled?: boolean,
|
||||
abortButtonDisabled?: boolean,
|
||||
onConfirmed?: any => any,
|
||||
onAborted?: any => any,
|
||||
onConfirmed?: (any) => any,
|
||||
onAborted?: (any) => any,
|
||||
className?: string,
|
||||
children?: React.Node,
|
||||
extraContent?: React.Node,
|
||||
|
@ -52,7 +52,13 @@ export function Modal(props: ModalProps) {
|
|||
>
|
||||
{title && <h1 className="card__title card__title--deprecated">{title}</h1>}
|
||||
{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}
|
||||
{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 Yrbl from 'component/yrbl';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
// $FlowFixMe cannot resolve ...
|
||||
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
||||
|
||||
|
@ -229,7 +230,9 @@ function ChannelPage(props: Props) {
|
|||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
||||
<h1 className="channel__title">
|
||||
{title || '@' + channelName}
|
||||
<TruncatedText lines={2} showTooltip>
|
||||
{title || '@' + channelName}
|
||||
</TruncatedText>
|
||||
<ChannelStakedIndicator uri={uri} large />
|
||||
</h1>
|
||||
<div className="channel__meta">
|
||||
|
|
|
@ -102,6 +102,8 @@ class RewardsPage extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
renderCustomRewardCode() {
|
||||
const { user } = this.props;
|
||||
const isNotEligible = !user || !user.primary_email || !user.has_verified_email || !user.is_reward_approved;
|
||||
return (
|
||||
<RewardTile
|
||||
key={REWARD_TYPES.TYPE_GENERATED_CODE}
|
||||
|
@ -110,6 +112,7 @@ class RewardsPage extends PureComponent<Props> {
|
|||
reward_title: __('Custom Code'),
|
||||
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 (
|
||||
<div
|
||||
aria-hidden={isNotEligible}
|
||||
className={classnames('card__list', {
|
||||
'card--disabled': isNotEligible,
|
||||
})}
|
||||
>
|
||||
{rewards.map((reward) => (
|
||||
<RewardTile key={reward.claim_code} reward={reward} />
|
||||
<RewardTile disabled={isNotEligible} key={reward.claim_code} reward={reward} />
|
||||
))}
|
||||
{this.renderCustomRewardCode()}
|
||||
</div>
|
||||
|
|
|
@ -236,6 +236,7 @@
|
|||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-button-alt-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
$cover-z-index: 0;
|
||||
$metadata-z-index: 1;
|
||||
$actions-z-index: 2;
|
||||
|
||||
.channel-cover {
|
||||
position: relative;
|
||||
|
@ -200,7 +201,7 @@ $metadata-z-index: 1;
|
|||
top: 0;
|
||||
right: var(--spacing-m);
|
||||
margin-top: var(--spacing-m);
|
||||
z-index: $metadata-z-index;
|
||||
z-index: $actions-z-index;
|
||||
flex-wrap: wrap;
|
||||
font-size: var(--font-base);
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
|
||||
&:hover {
|
||||
.claim__menu-button {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,7 +269,7 @@
|
|||
.claim-preview__title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-body);
|
||||
margin-right: auto;
|
||||
margin-right: var(--spacing-l);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
|
@ -475,7 +475,7 @@
|
|||
cursor: pointer;
|
||||
|
||||
.claim__menu-button {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
.collection-preview__overlay-thumbs {
|
||||
opacity: 1;
|
||||
|
@ -531,7 +531,7 @@
|
|||
.claim-tile__title {
|
||||
position: relative;
|
||||
padding: var(--spacing-s);
|
||||
padding-right: var(--spacing-l);
|
||||
padding-right: var(--spacing-xl);
|
||||
padding-bottom: 0;
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
||||
|
@ -540,14 +540,8 @@
|
|||
font-size: var(--font-small);
|
||||
min-height: 2rem;
|
||||
|
||||
.claim__menu-button {
|
||||
right: 0.2rem;
|
||||
top: var(--spacing-s);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
min-height: 2.5rem;
|
||||
padding-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -745,29 +739,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim__menu-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-xs);
|
||||
right: var(--spacing-xs);
|
||||
.claim-tile__header {
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
stroke: var(--color-text);
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
&:not([aria-expanded='true']) {
|
||||
display: none;
|
||||
}
|
||||
.claim__menu-button {
|
||||
right: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.claim__menu-button--inline {
|
||||
position: relative;
|
||||
display: block;
|
||||
right: auto;
|
||||
top: auto;
|
||||
@extend .button--alt;
|
||||
padding: 0 var(--spacing-xxs);
|
||||
.menu__button {
|
||||
&.claim__menu-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-xs);
|
||||
right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
&.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 {
|
||||
|
|
|
@ -263,14 +263,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
.comment__menu {
|
||||
align-self: flex-end;
|
||||
line-height: 1;
|
||||
|
||||
button {
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&:focus {
|
||||
@include linkFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
box-shadow: var(--card-box-shadow);
|
||||
border-radius: var(--card-radius);
|
||||
|
|
|
@ -9,6 +9,25 @@
|
|||
-webkit-user-select: none;
|
||||
-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;
|
||||
}
|
||||
|
|
|
@ -318,7 +318,7 @@ $discussion-header__height: 3rem;
|
|||
padding-bottom: var(--spacing-xxs);
|
||||
.markdown-preview {
|
||||
p {
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -218,14 +218,6 @@ $contentMaxWidth: 60rem;
|
|||
.icon {
|
||||
stroke: var(--color-text-help);
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&:focus {
|
||||
@include linkFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification__filter {
|
||||
|
|
|
@ -44,10 +44,25 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
opacity: 1;
|
||||
background-color: var(--color-button-alt-bg);
|
||||
.icon {
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
stroke: var(--color-menu-hovering);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,10 +124,10 @@
|
|||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-background--active: var(--color-card-background-highlighted);
|
||||
--color-menu-icon: var(--color-navigation-link);
|
||||
--color-menu: var(--color-gray-3);
|
||||
--color-menu-hovering: var(--color-gray-6);
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: var(--color-gray-3);
|
||||
--color-comment-menu-hovering: var(--color-gray-6);
|
||||
--color-comment-highlighted: #fff2d9;
|
||||
--color-comment-threadline: var(--color-gray-1);
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
|
|
|
@ -90,10 +90,10 @@
|
|||
--color-menu-background: var(--color-header-background);
|
||||
--color-menu-background--active: var(--color-gray-7);
|
||||
--color-menu-icon: var(--color-gray-4);
|
||||
--color-menu: var(--color-gray-5);
|
||||
--color-menu-hovering: var(--color-gray-2);
|
||||
|
||||
// Comments
|
||||
--color-comment-menu: var(--color-gray-5);
|
||||
--color-comment-menu-hovering: var(--color-gray-2);
|
||||
--color-comment-threadline: #434b54;
|
||||
--color-comment-threadline-hover: var(--color-gray-4);
|
||||
--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"
|
||||
|
||||
react@^16.8.2:
|
||||
version "16.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
|
||||
version "16.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
||||
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
|
Loading…
Reference in a new issue