#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",
"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--"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -331,6 +331,7 @@ function SideNavigation(props: Props) {
>
{!isOnFilePage && (
<nav
aria-label={'Sidebar'}
className={classnames('navigation', {
'navigation--micro': microNavigation,
// @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,
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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -318,7 +318,7 @@ $discussion-header__height: 3rem;
padding-bottom: var(--spacing-xxs);
.markdown-preview {
p {
word-break: break-all;
word-break: break-word;
}
}
}

View file

@ -218,14 +218,6 @@ $contentMaxWidth: 60rem;
.icon {
stroke: var(--color-text-help);
}
button {
border-radius: var(--border-radius);
&:focus {
@include linkFocus;
}
}
}
.notification__filter {

View file

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

View file

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

View file

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

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