mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-08 13:01:10 +02:00
New: Screen reader Accessibility Improvements
This commit is contained in:
parent
33a5ccd794
commit
ffaaf5270d
51 changed files with 199 additions and 37 deletions
|
|
@ -137,10 +137,15 @@ function BlocklistRow({
|
|||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
aria-label={translate('Details')}
|
||||
onPress={handleDetailsPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
aria-label={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isRemoving}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { useSingleSeries } from 'Series/useSeries';
|
|||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import styles from './HistoryRow.css';
|
||||
|
|
@ -221,7 +222,11 @@ function HistoryRow(props: HistoryRowProps) {
|
|||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.details}>
|
||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
aria-label={translate('Details')}
|
||||
onPress={handleDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export function useQueueItemForEpisode(episodeId: number) {
|
|||
const queue = useContext(QueueDetailsContext);
|
||||
|
||||
return useMemo(() => {
|
||||
return queue?.find((item) => item.episodeIds.includes(episodeId));
|
||||
return queue?.find((item) => item.episodeIds?.includes(episodeId));
|
||||
}, [episodeId, queue]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -369,6 +369,7 @@ function QueueRow(props: QueueRowProps) {
|
|||
{showInteractiveImport ? (
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
aria-label={translate('InteractiveSearch')}
|
||||
onPress={handleInteractiveImportPress}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -377,6 +378,7 @@ function QueueRow(props: QueueRowProps) {
|
|||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
aria-label={translate('Grab')}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={handleGrabPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,13 @@ function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) {
|
|||
|
||||
return (
|
||||
<div className={styles.searchResult}>
|
||||
<Link className={styles.underlay} {...linkProps} />
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
aria-label={
|
||||
isExistingSeries ? title : translate('AddSeriesWithTitle', { title })
|
||||
}
|
||||
{...linkProps}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
{isSmallScreen ? null : (
|
||||
|
|
@ -113,12 +119,14 @@ function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) {
|
|||
<Link
|
||||
className={styles.tvdbLink}
|
||||
to={`https://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
aria-label={translate('ViewSeriesOnTvdb', { title })}
|
||||
onPress={handleTvdbLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tvdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { DateFilterValue, FilterType } from 'Helpers/Props/filterTypes';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import DefaultFilterBuilderRowValue from './DefaultFilterBuilderRowValue';
|
||||
|
|
@ -300,11 +301,16 @@ function FilterBuilderRow<T>({
|
|||
<div className={styles.actionsContainer}>
|
||||
<IconButton
|
||||
name={icons.SUBTRACT}
|
||||
aria-label={translate('Remove')}
|
||||
isDisabled={filterCount === 1}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
|
||||
<IconButton name={icons.ADD} onPress={handleAddPress} />
|
||||
<IconButton
|
||||
name={icons.ADD}
|
||||
aria-label={translate('Add')}
|
||||
onPress={handleAddPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,11 @@ function CustomFilter({
|
|||
<div className={styles.label}>{label}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<IconButton name={icons.EDIT} onPress={handleEditPress} />
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
aria-label={translate('Edit')}
|
||||
onPress={handleEditPress}
|
||||
/>
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFilter')}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './KeyValueListInputItem.css';
|
||||
|
||||
|
|
@ -77,6 +78,7 @@ function KeyValueListInputItem({
|
|||
{isNew ? null : (
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
aria-label={translate('Remove')}
|
||||
tabIndex={-1}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import Link from 'Components/Link/Link';
|
||||
import MiddleTruncate from 'Components/MiddleTruncate';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { TagBase } from './TagInput';
|
||||
import styles from './TagInputTag.css';
|
||||
|
||||
|
|
@ -66,6 +67,7 @@ function TagInputTag<T extends TagBase>({
|
|||
<IconButton
|
||||
className={styles.editButton}
|
||||
name={icons.EDIT}
|
||||
aria-label={translate('Edit')}
|
||||
size={9}
|
||||
onPress={handleEdit}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Icon, { IconProps } from 'Components/Icon';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Link, { LinkProps } from './Link';
|
||||
import styles from './IconButton.css';
|
||||
|
||||
|
|
@ -26,7 +25,6 @@ export default function IconButton({
|
|||
className,
|
||||
otherProps.isDisabled && styles.isDisabled
|
||||
)}
|
||||
aria-label={translate('TableOptionsButton')}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -35,6 +33,7 @@ export default function IconButton({
|
|||
kind={kind}
|
||||
size={size}
|
||||
isSpinning={isSpinning}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { Size } from 'Helpers/Props/sizes';
|
|||
import { isIOS } from 'Utilities/browser';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import { ModalContext } from './ModalContext';
|
||||
import ModalError from './ModalError';
|
||||
import styles from './Modal.css';
|
||||
|
||||
|
|
@ -163,26 +164,36 @@ function Modal({
|
|||
return null;
|
||||
}
|
||||
|
||||
const headerId = `${modalId}-header`;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<FocusLock disabled={false}>
|
||||
<div className={styles.modalContainer}>
|
||||
<div
|
||||
ref={backgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={handleBackdropBeginPress}
|
||||
onMouseUp={handleBackdropEndPress}
|
||||
>
|
||||
<div className={classNames(className, styles[size])} style={style}>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
<ModalContext.Provider value={{ headerId }}>
|
||||
<FocusLock disabled={false}>
|
||||
<div className={styles.modalContainer}>
|
||||
<div
|
||||
ref={backgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={handleBackdropBeginPress}
|
||||
onMouseUp={handleBackdropEndPress}
|
||||
>
|
||||
<div
|
||||
className={classNames(className, styles[size])}
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={headerId}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>,
|
||||
</FocusLock>
|
||||
</ModalContext.Provider>,
|
||||
node!
|
||||
);
|
||||
}
|
||||
|
|
|
|||
11
frontend/src/Components/Modal/ModalContext.ts
Normal file
11
frontend/src/Components/Modal/ModalContext.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface ModalContextValue {
|
||||
headerId: string;
|
||||
}
|
||||
|
||||
export const ModalContext = createContext<ModalContextValue>({ headerId: '' });
|
||||
|
||||
export function useModalContext() {
|
||||
return useContext(ModalContext);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { ForwardedRef, forwardRef, ReactNode } from 'react';
|
||||
import { useModalContext } from './ModalContext';
|
||||
import styles from './ModalHeader.css';
|
||||
|
||||
interface ModalHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
|
@ -10,8 +11,15 @@ const ModalHeader = forwardRef(
|
|||
{ children, ...otherProps }: ModalHeaderProps,
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
const { headerId } = useModalContext();
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.modalHeader} {...otherProps}>
|
||||
<div
|
||||
ref={ref}
|
||||
id={headerId}
|
||||
className={styles.modalHeader}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ function MonitorToggleButton(props: MonitorToggleButtonProps) {
|
|||
name={iconName}
|
||||
size={size}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
isDisabled={isDisabled}
|
||||
isSpinning={isSaving}
|
||||
{...otherProps}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ function PageHeader() {
|
|||
<IconButton
|
||||
id="sidebar-toggle-button"
|
||||
name={icons.NAVBAR_COLLAPSE}
|
||||
aria-label={translate('Menu')}
|
||||
onPress={handleSidebarToggle}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -435,10 +435,11 @@ function PageSidebar() {
|
|||
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<div
|
||||
<nav
|
||||
ref={sidebarRef}
|
||||
className={styles.sidebarContainer}
|
||||
style={containerStyle}
|
||||
aria-label={translate('MainNavigation')}
|
||||
>
|
||||
{isSmallScreen ? (
|
||||
<div className={styles.sidebarHeader}>
|
||||
|
|
@ -521,7 +522,7 @@ function PageSidebar() {
|
|||
|
||||
<Messages />
|
||||
</ScrollerComponent>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,11 +46,12 @@ function PageSidebarItem({
|
|||
isActive && styles.isActiveLink
|
||||
)}
|
||||
to={to}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{!!iconName && (
|
||||
<span className={styles.iconContainer}>
|
||||
<Icon name={iconName} />
|
||||
<Icon name={iconName} aria-hidden={true} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { icons, scrollDirections } from 'Helpers/Props';
|
|||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Column from './Column';
|
||||
import TableHeader from './TableHeader';
|
||||
import TableHeaderCell from './TableHeaderCell';
|
||||
|
|
@ -94,7 +95,10 @@ function Table({
|
|||
canModifyColumns={canModifyColumns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
aria-label={translate('AdvancedSettings')}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</TableHeaderCell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
|
|
@ -41,6 +41,20 @@ function TableHeaderCell({
|
|||
? icons.SORT_ASCENDING
|
||||
: icons.SORT_DESCENDING;
|
||||
|
||||
const ariaSortValue = useMemo(() => {
|
||||
if (!isSortable) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!isSorting) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return sortDirection === sortDirections.ASCENDING
|
||||
? 'ascending'
|
||||
: 'descending';
|
||||
}, [isSorting, sortDirection, isSortable]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (fixedSortDirection) {
|
||||
onSortPress?.(name, fixedSortDirection);
|
||||
|
|
@ -56,14 +70,20 @@ function TableHeaderCell({
|
|||
className={className}
|
||||
// label={typeof label === 'function' ? label() : label}
|
||||
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
|
||||
scope="col"
|
||||
aria-sort={ariaSortValue}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{children}
|
||||
|
||||
{isSorting && <Icon name={sortIcon} className={styles.sortIcon} />}
|
||||
{isSorting ? (
|
||||
<Icon name={sortIcon} className={styles.sortIcon} aria-hidden={true} />
|
||||
) : null}
|
||||
</Link>
|
||||
) : (
|
||||
<th className={className}>{children}</th>
|
||||
<th className={className} scope="col">
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,9 +108,10 @@ function TablePager({
|
|||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
aria-label={translate('PagerGoToFirstPage')}
|
||||
onPress={handleFirstPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_FIRST} />
|
||||
<Icon name={icons.PAGE_FIRST} aria-hidden={true} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
|
|
@ -119,15 +120,20 @@ function TablePager({
|
|||
isFirstPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isFirstPage}
|
||||
aria-label={translate('PagerGoToPreviousPage')}
|
||||
onPress={onPreviousPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_PREVIOUS} />
|
||||
<Icon name={icons.PAGE_PREVIOUS} aria-hidden={true} />
|
||||
</Link>
|
||||
|
||||
<div className={styles.pageNumber}>
|
||||
{isShowingPageSelect ? null : (
|
||||
<Link
|
||||
isDisabled={totalPages === 1}
|
||||
aria-label={translate('PagerGoToPage', {
|
||||
page,
|
||||
totalPages: totalPages ?? 0,
|
||||
})}
|
||||
onPress={handleOpenPageSelectClick}
|
||||
>
|
||||
{page} / {totalPages}
|
||||
|
|
@ -153,9 +159,10 @@ function TablePager({
|
|||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
aria-label={translate('PagerGoToNextPage')}
|
||||
onPress={onNextPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_NEXT} />
|
||||
<Icon name={icons.PAGE_NEXT} aria-hidden={true} />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
|
|
@ -164,9 +171,10 @@ function TablePager({
|
|||
isLastPage && styles.disabledPageButton
|
||||
)}
|
||||
isDisabled={isLastPage}
|
||||
aria-label={translate('PagerGoToLastPage')}
|
||||
onPress={onLastPagePress}
|
||||
>
|
||||
<Icon name={icons.PAGE_LAST} />
|
||||
<Icon name={icons.PAGE_LAST} aria-hidden={true} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ function EpisodeSearchCell({
|
|||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('InteractiveSearch')}
|
||||
aria-label={translate('InteractiveSearch')}
|
||||
onPress={setDetailsModalOpen}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ function EpisodeHistoryRow({
|
|||
{eventType === 'grabbed' && (
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
aria-label={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={handleMarkAsFailedPress}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ function EpisodeFileRow(props: EpisodeFileRowProps) {
|
|||
|
||||
<IconButton
|
||||
title={translate('DeleteEpisodeFromDisk')}
|
||||
aria-label={translate('DeleteEpisodeFromDisk')}
|
||||
name={icons.REMOVE}
|
||||
onPress={setRemoveEpisodeFileModalOpen}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
|
|||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('FavoriteFolderRemove')}
|
||||
aria-label={translate('FavoriteFolderRemove')}
|
||||
kind="danger"
|
||||
name={icons.HEART}
|
||||
onPress={handleRemoveFavoritePress}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ function RecentFolderRow({
|
|||
? translate('FavoriteFolderRemove')
|
||||
: translate('FavoriteFolderAdd')
|
||||
}
|
||||
aria-label={
|
||||
isFavorite
|
||||
? translate('FavoriteFolderRemove')
|
||||
: translate('FavoriteFolderAdd')
|
||||
}
|
||||
kind={isFavorite ? 'danger' : 'default'}
|
||||
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
|
||||
onPress={handleFavoritePress}
|
||||
|
|
@ -71,6 +76,7 @@ function RecentFolderRow({
|
|||
|
||||
<IconButton
|
||||
title={translate('Remove')}
|
||||
aria-label={translate('Remove')}
|
||||
name={icons.REMOVE}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ function RootFolderRow(props: RootFolderRowProps) {
|
|||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('RemoveRootFolder')}
|
||||
aria-label={translate('RemoveRootFolder')}
|
||||
name={icons.REMOVE}
|
||||
onPress={onDeletePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -574,6 +574,9 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
title={translate('SeriesDetailsGoTo', {
|
||||
title: previousSeries.title,
|
||||
})}
|
||||
aria-label={translate('SeriesDetailsGoTo', {
|
||||
title: previousSeries.title,
|
||||
})}
|
||||
to={`/series/${previousSeries.titleSlug}`}
|
||||
/>
|
||||
) : null}
|
||||
|
|
@ -586,6 +589,9 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
title={translate('SeriesDetailsGoTo', {
|
||||
title: nextSeries.title,
|
||||
})}
|
||||
aria-label={translate('SeriesDetailsGoTo', {
|
||||
title: nextSeries.title,
|
||||
})}
|
||||
to={`/series/${nextSeries.titleSlug}`}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -432,6 +432,7 @@ function SeriesDetailsSeason({
|
|||
className={styles.actionButton}
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('InteractiveSearchSeason')}
|
||||
aria-label={translate('InteractiveSearchSeason')}
|
||||
size={24}
|
||||
isDisabled={!totalEpisodeCount}
|
||||
onPress={handleInteractiveSearchPress}
|
||||
|
|
@ -441,6 +442,7 @@ function SeriesDetailsSeason({
|
|||
className={styles.actionButton}
|
||||
name={icons.ORGANIZE}
|
||||
title={translate('PreviewRenameSeason')}
|
||||
aria-label={translate('PreviewRenameSeason')}
|
||||
size={24}
|
||||
isDisabled={!episodeFileCount}
|
||||
onPress={handleOrganizePress}
|
||||
|
|
@ -450,6 +452,7 @@ function SeriesDetailsSeason({
|
|||
className={styles.actionButton}
|
||||
name={icons.EPISODE_FILE}
|
||||
title={translate('ManageEpisodesSeason')}
|
||||
aria-label={translate('ManageEpisodesSeason')}
|
||||
size={24}
|
||||
isDisabled={!episodeFileCount}
|
||||
onPress={handleManageEpisodesPress}
|
||||
|
|
@ -459,6 +462,7 @@ function SeriesDetailsSeason({
|
|||
className={styles.actionButton}
|
||||
name={icons.HISTORY}
|
||||
title={translate('HistorySeason')}
|
||||
aria-label={translate('HistorySeason')}
|
||||
size={24}
|
||||
isDisabled={!totalEpisodeCount}
|
||||
onPress={handleHistoryPress}
|
||||
|
|
@ -506,6 +510,7 @@ function SeriesDetailsSeason({
|
|||
name={icons.COLLAPSE}
|
||||
size={20}
|
||||
title={translate('HideEpisodes')}
|
||||
aria-label={translate('HideEpisodes')}
|
||||
onPress={handleExpandPress}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ function SeriesHistoryRow({
|
|||
{eventType === 'grabbed' ? (
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
aria-label={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={handleMarkAsFailedPress}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
|
|||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditSeries')}
|
||||
aria-label={translate('EditSeries')}
|
||||
onPress={onEditSeriesPress}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditSeries')}
|
||||
aria-label={translate('EditSeries')}
|
||||
tabIndex={-1}
|
||||
onPress={onEditSeriesPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -490,6 +490,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditSeries')}
|
||||
aria-label={translate('EditSeries')}
|
||||
onPress={onEditSeriesPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from 'Series/seriesOptionsStore';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import SeriesIndexTableOptions from './SeriesIndexTableOptions';
|
||||
import styles from './SeriesIndexTableHeader.css';
|
||||
|
|
@ -95,7 +96,10 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) {
|
|||
optionsComponent={SeriesIndexTableOptions}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
aria-label={translate('AdvancedSettings')}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ function CustomFormat({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCustomFormat')}
|
||||
aria-label={translate('CloneCustomFormat')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneCustomFormatPressHandler}
|
||||
/>
|
||||
|
|
@ -90,6 +91,7 @@ function CustomFormat({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('ExportCustomFormat')}
|
||||
aria-label={translate('ExportCustomFormat')}
|
||||
name={icons.EXPORT}
|
||||
onPress={handleExportCustomFormatPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ function ManageCustomFormatsModalRow({
|
|||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
aria-label={translate('Edit')}
|
||||
onPress={handleEditCustomFormatModalOpen}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ function Specification({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCondition')}
|
||||
aria-label={translate('CloneCondition')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneSpecificationPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ function ImportListExclusionRow({
|
|||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
aria-label={translate('Edit')}
|
||||
onPress={setEditImportListExclusionModalOpen}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ function ImportListExclusionsContent() {
|
|||
<TableRowCell>
|
||||
<IconButton
|
||||
name={icons.ADD}
|
||||
aria-label={translate('Add')}
|
||||
onPress={setAddImportListExclusionModalOpen}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ function ImportList({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneImportList')}
|
||||
aria-label={translate('CloneImportList')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneImportListPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ function Indexer({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneIndexer')}
|
||||
aria-label={translate('CloneIndexer')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneIndexerPress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ function QualityProfile({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneProfile')}
|
||||
aria-label={translate('CloneProfile')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneQualityProfilePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ function QualityProfileItem({
|
|||
className={styles.createGroupButton}
|
||||
name={icons.GROUP}
|
||||
title={translate('Group')}
|
||||
aria-label={translate('Group')}
|
||||
onPress={handleCreateGroupPress}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ function QualityProfileItemGroup({
|
|||
className={styles.deleteGroupButton}
|
||||
name={icons.UNGROUP}
|
||||
title={translate('Ungroup')}
|
||||
aria-label={translate('Ungroup')}
|
||||
onPress={handleDeleteGroupPress}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export default function AutoTagging({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneAutoTag')}
|
||||
aria-label={translate('CloneAutoTag')}
|
||||
name={icons.CLONE}
|
||||
onPress={onClonePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export default function Specification({
|
|||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('Clone')}
|
||||
aria-label={translate('Clone')}
|
||||
name={icons.CLONE}
|
||||
onPress={onClonePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -96,12 +96,14 @@ function BackupRow({ id, type, name, path, size, time }: BackupRowProps) {
|
|||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('RestoreBackup')}
|
||||
aria-label={translate('RestoreBackup')}
|
||||
name={icons.RESTORE}
|
||||
onPress={handleRestorePress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('DeleteBackup')}
|
||||
aria-label={translate('DeleteBackup')}
|
||||
name={icons.DELETE}
|
||||
onPress={handleDeletePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ function Health() {
|
|||
name={icons.WIKI}
|
||||
to={item.wikiUrl}
|
||||
title={translate('ReadTheWikiForMoreInformation')}
|
||||
aria-label={translate('ReadTheWikiForMoreInformation')}
|
||||
/>
|
||||
|
||||
<HealthItemLink source={source} />
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ function HealthItemLink(props: HealthItemLinkProps) {
|
|||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
aria-label={translate('Settings')}
|
||||
to="/settings/indexers"
|
||||
/>
|
||||
);
|
||||
|
|
@ -30,6 +31,7 @@ function HealthItemLink(props: HealthItemLinkProps) {
|
|||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
aria-label={translate('Settings')}
|
||||
to="/settings/downloadclients"
|
||||
/>
|
||||
);
|
||||
|
|
@ -38,6 +40,7 @@ function HealthItemLink(props: HealthItemLinkProps) {
|
|||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
title={translate('Settings')}
|
||||
aria-label={translate('Settings')}
|
||||
to="/settings/connect"
|
||||
/>
|
||||
);
|
||||
|
|
@ -46,6 +49,7 @@ function HealthItemLink(props: HealthItemLinkProps) {
|
|||
<IconButton
|
||||
name={icons.SERIES_CONTINUING}
|
||||
title={translate('SeriesEditor')}
|
||||
aria-label={translate('SeriesEditor')}
|
||||
to="/serieseditor"
|
||||
/>
|
||||
);
|
||||
|
|
@ -54,6 +58,7 @@ function HealthItemLink(props: HealthItemLinkProps) {
|
|||
<IconButton
|
||||
name={icons.UPDATE}
|
||||
title={translate('Updates')}
|
||||
aria-label={translate('Updates')}
|
||||
to="/system/updates"
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -219,6 +219,7 @@ export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
|||
{status === 'queued' && (
|
||||
<IconButton
|
||||
title={translate('RemovedFromTaskQueue')}
|
||||
aria-label={translate('RemovedFromTaskQueue')}
|
||||
name={icons.REMOVE}
|
||||
onPress={openCancelConfirmModal}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { isCommandExecuting } from 'Utilities/Command';
|
|||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ScheduledTaskRow.css';
|
||||
|
||||
interface ScheduledTaskRowProps {
|
||||
|
|
@ -138,6 +139,7 @@ function ScheduledTaskRow({
|
|||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
aria-label={translate('Run')}
|
||||
isSpinning={isExecuting}
|
||||
onPress={handleExecutePress}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"AddedDate": "Added: {date}",
|
||||
"AddedToDownloadQueue": "Added to download queue",
|
||||
"AddingTag": "Adding tag",
|
||||
"AdvancedSettings": "Advanced Settings",
|
||||
"AfterManualRefresh": "After Manual Refresh",
|
||||
"Age": "Age",
|
||||
"AgeWhenGrabbed": "Age (when grabbed)",
|
||||
|
|
@ -1182,6 +1183,7 @@
|
|||
"Logs": "Logs",
|
||||
"LongDateFormat": "Long Date Format",
|
||||
"Lowercase": "Lowercase",
|
||||
"MainNavigation": "Main Navigation",
|
||||
"MaintenanceRelease": "Maintenance Release: bug fixes and other improvements. See Github Commit History for more details",
|
||||
"ManageClients": "Manage Clients",
|
||||
"ManageCustomFormats": "Manage Custom Formats",
|
||||
|
|
@ -1635,6 +1637,11 @@
|
|||
"OverviewOptions": "Overview Options",
|
||||
"PackageVersion": "Package Version",
|
||||
"PackageVersionInfo": "{packageVersion} by {packageAuthor}",
|
||||
"PagerGoToFirstPage": "Go to first page",
|
||||
"PagerGoToLastPage": "Go to last page",
|
||||
"PagerGoToNextPage": "Go to next page",
|
||||
"PagerGoToPage": "Go to page {page} of {totalPages}",
|
||||
"PagerGoToPreviousPage": "Go to previous page",
|
||||
"Parse": "Parse",
|
||||
"ParseModalErrorParsing": "Error parsing, please try again.",
|
||||
"ParseModalHelpText": "Enter a release title in the input above",
|
||||
|
|
@ -1875,6 +1882,7 @@
|
|||
"RssSyncInterval": "RSS Sync Interval",
|
||||
"RssSyncIntervalHelpText": "Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)",
|
||||
"RssSyncIntervalHelpTextWarning": "This will apply to all indexers, please follow the rules set forth by them",
|
||||
"Run": "Run",
|
||||
"Runtime": "Runtime",
|
||||
"Saturday": "Saturday",
|
||||
"Save": "Save",
|
||||
|
|
@ -2232,6 +2240,7 @@
|
|||
"VideoCodec": "Video Codec",
|
||||
"VideoDynamicRange": "Video Dynamic Range",
|
||||
"View": "View",
|
||||
"ViewSeriesOnTvdb": "View {title} on TVDB",
|
||||
"VisitTheWikiForMoreDetails": "Visit the wiki for more details: ",
|
||||
"WaitingToImport": "Waiting to Import",
|
||||
"WaitingToProcess": "Waiting to Process",
|
||||
|
|
|
|||
Loading…
Reference in a new issue