Add alternative text to controls to allow screen readers to read them aloud (#1840)

Add alternative text to controls to allow screen readers to read them aloud.

Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Florian Scholdei
2021-11-03 10:11:40 +01:00
committed by GitHub
parent b78742ed0b
commit b896df5046
49 changed files with 1274 additions and 553 deletions

View File

@@ -0,0 +1,2 @@
- type: added
descripion: Add alternative text to controls to allow screen readers to read them aloud ([#1840](https://github.com/scm-manager/scm-manager/pull/1840))

View File

@@ -44,7 +44,7 @@ const baseUrl = "scm-manager.org/scm/repo/hitchhiker/heartOfGold/sources";
const sources = Git;
const prefix = (
<a href="#link">
<Icon name="heart" color="danger" />
<Icon name="heart" color="danger" alt="heart icon" />
</a>
);

View File

@@ -194,10 +194,10 @@ const Breadcrumb: FC<Props> = ({
</ul>
<PermaLinkWrapper className="ml-1">
{copying ? (
<Icon name="spinner fa-spin" />
<Icon name="spinner fa-spin" alt={t("breadcrumb.loading")} />
) : (
<Tooltip message={t("breadcrumb.copyPermalink")}>
<Icon name="link" color="inherit" onClick={() => copySource()} />
<Icon name="link" color="inherit" onClick={() => copySource()} alt={t("breadcrumb.copyPermalink")} />
</Tooltip>
)}
</PermaLinkWrapper>

View File

@@ -36,7 +36,7 @@ const Wrapper = styled.div`
`;
const link = "/foo/bar";
const avatar = <Icon name="icons fa-2x fa-fw" />;
const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
const title = <strong>title</strong>;
const footerLeft = <small>left footer</small>;
const footerRight = <small>right footer</small>;

View File

@@ -24,8 +24,10 @@
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
import classNames from "classnames";
import Icon from "./Icon";
import { withTranslation, WithTranslation } from "react-i18next";
type Props = {
type Props = WithTranslation & {
name: ReactNode;
url?: string;
elements: ReactNode[];
@@ -35,7 +37,7 @@ type State = {
collapsed: boolean;
};
export default class CardColumnGroup extends React.Component<Props, State> {
class CardColumnGroup extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@@ -62,12 +64,13 @@ export default class CardColumnGroup extends React.Component<Props, State> {
};
render() {
const { name, url, elements } = this.props;
const { name, url, elements, t } = this.props;
const { collapsed } = this.state;
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
let icon = <Icon name="angle-right" color="inherit" alt={t("cardColumnGroup.showContent")} />;
let content = null;
if (!collapsed) {
icon = <Icon name="angle-down" color="inherit" alt={t("cardColumnGroup.hideContent")} />;
content = elements.map((entry, index) => {
const fullColumnWidth = this.isFullSize(elements, index);
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
@@ -83,7 +86,7 @@ export default class CardColumnGroup extends React.Component<Props, State> {
<div className="mb-4">
<h2>
<span className={classNames("is-size-4", "is-clickable")} onClick={this.toggleCollapse}>
<i className={classNames("fa", icon)} />
{icon}
</span>{" "}
{url ? (
<Link to={url} className="has-text-dark">
@@ -100,3 +103,5 @@ export default class CardColumnGroup extends React.Component<Props, State> {
);
}
}
export default withTranslation("commons")(CardColumnGroup);

View File

@@ -34,7 +34,7 @@ const Wrapper = styled.div`
`;
const link = "/foo/bar";
const icon = <Icon name="icons fa-2x fa-fw" />;
const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
const contentLeft = <strong className="m-0">main content</strong>;
const contentRight = <small>more text</small>;
@@ -42,13 +42,13 @@ storiesOf("CardColumnSmall", module)
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.addDecorator((storyFn) => <Wrapper>{storyFn()}</Wrapper>)
.add("Default", () => (
<CardColumnSmall link={link} avatar={icon} contentLeft={contentLeft} contentRight={contentRight} />
<CardColumnSmall link={link} avatar={avatar} contentLeft={contentLeft} contentRight={contentRight} />
))
.add("Minimal", () => <CardColumnSmall link={link} contentLeft={contentLeft} contentRight={contentRight} />)
.add("Task", () => (
<CardColumnSmall
link={link}
avatar={<Icon name="exchange-alt" className="fa-fw fa-lg" color="inherit" />}
avatar={<Icon name="exchange-alt" className="fa-fw fa-lg" color="inherit" alt="avatar" />}
contentLeft={<strong>Repository created</strong>}
contentRight={<small>over 42 years ago</small>}
footer="New: scmadmin/spaceship"

View File

@@ -75,7 +75,7 @@ const RedirectPage = () => {
"is-align-items-center"
)}
>
<Icon name="directions" className="fa-7x" />
<Icon name="directions" className="fa-7x" alt="" />
</RedirectIconContainer>
</div>
</section>

View File

@@ -28,6 +28,6 @@ type Props = {
className?: string;
};
const HelpIcon: FC<Props> = ({ className }) => <Icon name="question-circle" color="blue-light" className={className} />;
const HelpIcon: FC<Props> = ({ className }) => <Icon name="question-circle" color="blue-light" className={className} alt="" />;
export default HelpIcon;

View File

@@ -35,6 +35,7 @@ type Props = {
onEnter?: (event: React.KeyboardEvent) => void;
testId?: string;
tabIndex?: number;
alt?: string;
};
const Icon: FC<Props> = ({
@@ -47,6 +48,7 @@ const Icon: FC<Props> = ({
testId,
tabIndex = -1,
onEnter,
alt = title,
}) => {
return (
<i
@@ -55,6 +57,7 @@ const Icon: FC<Props> = ({
title={title}
className={classNames(iconStyle, "fa-fw", "fa-" + name, `has-text-${color}`, className)}
tabIndex={tabIndex}
aria-label={alt}
{...createAttributesForTesting(testId)}
/>
);

View File

@@ -31,12 +31,12 @@ storiesOf("SplitAndReplace", module).add("Simple replacement", () => {
const replacements = [
{
textToReplace: "'",
replacement: <Icon name={"quote-left"} />,
replacement: <Icon name="quote-left" alt="" />,
replaceAll: true,
},
{
textToReplace: "`",
replacement: <Icon name={"quote-right"} />,
replacement: <Icon name="quote-right" alt="" />,
replaceAll: true,
},
];

View File

@@ -78,7 +78,7 @@ const SyntaxHighlighterRenderer: FC<Props> = ({
stylesheet,
useInlineStyles,
createLinePermaLink,
showLineNumbers = true
showLineNumbers = true,
}) => {
const location = useLocation();
const history = useHistory();
@@ -108,7 +108,7 @@ const SyntaxHighlighterRenderer: FC<Props> = ({
node,
stylesheet,
useInlineStyles,
key: `code-segment${i}`
key: `code-segment${i}`,
});
return (
<RowContainer
@@ -119,10 +119,14 @@ const SyntaxHighlighterRenderer: FC<Props> = ({
{showLineNumbers && (
<>
{copying ? (
<Icon name="spinner fa-spin" />
<Icon name="spinner fa-spin" alt={t("sources.content.loading")} />
) : (
<Tooltip message={t("sources.content.copyPermalink")}>
<Icon name="link" onClick={() => lineNumberClick(lineNumber)} />
<Icon
name="link"
onClick={() => lineNumberClick(lineNumber)}
alt={t("sources.content.copyPermalink")}
/>
</Tooltip>
)}
<span
@@ -143,7 +147,7 @@ const SyntaxHighlighterRenderer: FC<Props> = ({
//
export const create = (createLinePermaLink: CreateLinePermaLinkFn, showLineNumbers = false): FC<Props> => {
return props => (
return (props) => (
<SyntaxHighlighterRenderer {...props} createLinePermaLink={createLinePermaLink} showLineNumbers={showLineNumbers} />
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
*/
import React, { MouseEvent, ReactNode } from "react";
import classNames from "classnames";
import { withRouter, RouteComponentProps } from "react-router-dom";
import { RouteComponentProps, withRouter } from "react-router-dom";
import Icon from "../Icon";
import { createAttributesForTesting } from "../devBuild";
@@ -51,7 +51,7 @@ type Props = ButtonProps &
class Button extends React.Component<Props> {
static defaultProps: Partial<Props> = {
type: "button",
color: "default"
color: "default",
};
onClick = (event: React.MouseEvent) => {
@@ -76,7 +76,7 @@ class Button extends React.Component<Props> {
fullWidth,
reducedMobile,
children,
testId
testId,
} = this.props;
if (icon) {
return (

View File

@@ -34,7 +34,7 @@ type Props = {
tooltipStyle?: "tooltipComponent" | "htmlTitle";
};
const Button = styled.a`
const Button = styled.button`
width: 50px;
&:hover {
color: #33b2e8;
@@ -52,6 +52,7 @@ const OpenInFullscreenButton: FC<Props> = ({ modalTitle, modalBody, tooltipStyle
title={tooltipStyle === "htmlTitle" ? tooltip : undefined}
className="button"
onClick={() => setShowModal(true)}
aria-label={tooltip}
>
<i className="fas fa-search-plus" />
</Button>

View File

@@ -31,7 +31,7 @@ import { Button, ButtonGroup } from "../buttons";
import copyToClipboard from "../CopyToClipboard";
const link = "/foo/bar";
const icon = <Icon name="icons fa-2x fa-fw" />;
const avatar = <Icon name="icons fa-2x fa-fw" alt="avatar" />;
const name = <strong className="m-0">main content</strong>;
const description = <small>more text</small>;
const longName = (
@@ -53,12 +53,12 @@ storiesOf("GroupEntry", module)
.addDecorator((story) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>)
.addDecorator((storyFn) => <div className="m-5">{storyFn()}</div>)
.add("Default", () => (
<GroupEntry link={link} avatar={icon} name={name} description={description} contentRight={contentRight} />
<GroupEntry link={link} avatar={avatar} name={name} description={description} contentRight={contentRight} />
))
.add("With long texts", () => (
<GroupEntry
link={link}
avatar={icon}
avatar={avatar}
name={longName}
description={
<small>

View File

@@ -88,10 +88,10 @@ const MarkdownHeadingRenderer: FC<Props> = ({ children, level, permalink, id })
.finally(() => setCopying(false));
};
const CopyButton = copying ? (
<Icon name="spinner fa-spin" />
<Icon name="spinner fa-spin" alt={t("sources.content.loading")} />
) : (
<Tooltip message={t("sources.content.copyPermalink")}>
<Icon name="link" onClick={copyPermalink} />
<Icon name="link" onClick={copyPermalink} alt={t("sources.content.copyPermalink")} />
</Tooltip>
);
const headingElement = React.createElement("h" + level, {id: anchorId}, [...reactChildren, CopyButton]);

View File

@@ -26,6 +26,7 @@ import React, { FC } from "react";
import styled from "styled-components";
import useMenuContext from "./MenuContext";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
type Props = {
label: string;
@@ -61,6 +62,7 @@ const MenuLabel = styled.p<CollapsedProps>`
`;
const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true }) => {
const [t] = useTranslation("commons");
const menuContext = useMenuContext();
const isCollapsed = collapsible && menuContext.isCollapsed();
@@ -77,6 +79,7 @@ const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true })
};
const arrowIcon = isCollapsed ? <i className="fas fa-caret-down" /> : <i className="fas fa-caret-right" />;
const menuAriaLabel = isCollapsed ? t("secondaryNavigation.showContent") : t("secondaryNavigation.hideContent");
return (
<SectionContainer className="menu">
@@ -85,6 +88,7 @@ const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true })
className={classNames("menu-label", { "is-clickable": collapsible })}
collapsed={isCollapsed}
onClick={toggleCollapseState}
aria-label={menuAriaLabel}
>
{collapsible ? (
<Icon color="info" className="is-medium" collapsed={isCollapsed}>

View File

@@ -40,9 +40,9 @@ const CommitAuthor: FC = () => {
return (
<>
{!me.mail && <Notification type="warning">{t("commit.commitAuthor.noMail")}</Notification>}
<span className="mb-2">
<div className="mb-2">
<strong>{t("commit.commitAuthor.author")}</strong> {`${me.displayName} <${mail}>`}
</span>
</div>
</>
);
};

View File

@@ -25,7 +25,7 @@ import React, { FC, MouseEvent } from "react";
import styled from "styled-components";
import Tooltip from "../Tooltip";
const Button = styled.a`
const Button = styled.button`
width: 50px;
&:hover {
color: #33b2e8;

View File

@@ -353,10 +353,11 @@ class DiffFile extends React.Component<Props, State> {
}
renderFileTitle = (file: FileDiff) => {
const { t } = this.props;
if (file.oldPath !== file.newPath && (file.type === "copy" || file.type === "rename")) {
return (
<>
{file.oldPath} <Icon name="arrow-right" color="inherit" /> {file.newPath}
{file.oldPath} <Icon name="arrow-right" color="inherit" alt={t("diff.renamedTo")} /> {file.newPath}
</>
);
} else if (file.type === "delete") {
@@ -426,13 +427,13 @@ class DiffFile extends React.Component<Props, State> {
</TokenizedDiffView>
</div>
);
let icon = "angle-right";
let icon = <Icon name="angle-right" color="inherit" alt={t("diff.showContent")} />;
let body = null;
if (!collapsed) {
icon = "angle-down";
icon = <Icon name="angle-down" color="inherit" alt={t("diff.hideContent")} />;
body = innerContent;
}
const collapseIcon = this.hasContent(file) ? <Icon name={icon} color="inherit" /> : null;
const collapseIcon = this.hasContent(file) ? icon : null;
const fileControls = fileControlFactory ? fileControlFactory(file, this.setCollapse) : null;
const modalTitle = file.type === "delete" ? file.oldPath : file.newPath;
const openInFullscreen = file?.hunks?.length ? (

View File

@@ -43,7 +43,7 @@ const JumpToFileButton: FC<Props> = ({ link, tooltip }) => {
return (
<Tooltip message={tooltip} location="top">
<Button aria-label={tooltip} className="button is-clickable" to={link}>
<Icon name="file-code" color="inherit" />
<Icon name="file-code" color="inherit" alt="" />
</Button>
</Tooltip>
);

View File

@@ -36,7 +36,7 @@ class RepositoryEntryLink extends React.Component<Props> {
render() {
const { to, icon, tooltip } = this.props;
let content = <Icon className="fa-lg" name={icon} color="inherit" />;
let content = <Icon className="fa-lg" name={icon} color="inherit" alt={`${icon} icon`} />;
if (tooltip) {
content = (
<Tooltip message={tooltip} location="top">

View File

@@ -31,7 +31,13 @@ type Props = {
};
const SortIcon: FC<Props> = (props: Props) => {
return <Icon className={classNames("ml-1", { "is-invisible": !props.isVisible })} name={props.name} />;
return (
<Icon
className={classNames("ml-1", { "is-invisible": !props.isVisible })}
name={props.name}
alt={`${props.name} icon`}
/>
);
};
export default SortIcon;

View File

@@ -36,6 +36,7 @@
"installedNavLink": "Installiert",
"availableNavLink": "Verfügbar"
},
"markedAsPending": "Als ausstehend markiert",
"showPending": "Änderungen anzeigen",
"executePending": "Änderungen ausführen",
"outdatedPlugins": "{{count}} Plugin aktualisieren",

View File

@@ -25,7 +25,8 @@
},
"breadcrumb": {
"home": "Hauptseite",
"copyPermalink": "Link in Zwischenablage kopieren"
"copyPermalink": "Link in Zwischenablage kopieren",
"loading": "Lade ..."
},
"errorNotification": {
"prefix": "Fehler",
@@ -59,6 +60,10 @@
"groups": "Gruppen",
"admin": "Administration"
},
"secondaryNavigation": {
"showContent": "Navigation vergrößern",
"hideContent": "Navigation verkleinern"
},
"filterEntries": "Einträge filtern",
"autocomplete": {
"group": "Gruppe",
@@ -133,10 +138,16 @@
"toastTitle": "Benachrichtigung",
"xMore": "+{{ count }} Benachrichtigung",
"xMore_plural": "+{{ count }} Benachrichtigungen",
"loading": "Lade ...",
"bellTitle": "Benachrichtigungen",
"empty": "Keine Benachrichtigungen",
"dismiss": "Löschen",
"dismissAll": "Alle löschen"
},
"cardColumnGroup": {
"showContent": "Inhalt einblenden",
"hideContent": "Inhalt ausblenden"
},
"duration": {
"ms": "{{count}} Millisekunde",
"ms_plural": "{{count}} Millisekunden",
@@ -179,6 +190,10 @@
"exampleValue": "Beispielwert",
"hints": "Hinweise"
},
"expandable": {
"showMore": "Mehr Informationen einblenden",
"hideMore": "Mehr Informationen ausblenden"
},
"exampleQueries": {
"title": "Beispielabfragen",
"description": "Felder mit Modifikatoren und Operatoren um Repositories zu finden.",
@@ -191,7 +206,7 @@
"utilities": {
"title": "Hilfsmittel",
"description": "Wandeln Sie menschlich-lesbare Zeitmarken in standard Millisekunden um, um diese in Ihren Queries zu verwenden. Das Datum ist ein Pflichtfeld, Stunde, Minute und Sekunde sind optional.",
"datetime":{
"datetime": {
"label": "Datum in Standardzeitstempel umwandeln",
"format": "yyyy-mm-dd hh:mm:ss",
"convertButtonLabel": "Konvertieren"

View File

@@ -46,7 +46,7 @@
},
"add-member-autocomplete": {
"placeholder": "Mitglied hinzufügen",
"loading": "Suche...",
"loading": "Suche ...",
"no-options": "Kein Vorschlag für Benutzername verfügbar"
},
"groupForm": {

View File

@@ -236,6 +236,8 @@
},
"contributors": {
"mailto": "Mail senden an",
"showList": "Liste der Mitwirkenden einblenden",
"hideList": "Liste der Mitwirkenden ausblenden",
"list": "Liste der Mitwirkenden",
"authoredBy": "Verfasst von",
"committedBy": "Committed von",
@@ -323,12 +325,15 @@
"showMarkdown": "Markdown rendern",
"showSources": "Sources anzeigen"
},
"showMore": "Mehr Informationen einblenden",
"hideMore": "Mehr Informationen ausblenden",
"path": "Pfad",
"branch": "Branch",
"commitDate": "Commitdatum",
"description": "Beschreibung",
"size": "Größe",
"copyPermalink": "Link in Zwischenablage kopieren"
"copyPermalink": "Link in Zwischenablage kopieren",
"loading": "Lade ..."
},
"noSources": "Keine Sources in diesem Branch gefunden.",
"extension": {
@@ -469,16 +474,20 @@
"fullscreen": {
"open": "In Vollbildansicht öffnen",
"close": "Schließen"
}
},
"renamedTo": "umbenannt in",
"showContent": "Inhalt des Diffs einblenden",
"hideContent": "Inhalt des Diffs ausblenden"
},
"fileUpload": {
"clickHere": "Klicken Sie hier um Ihre Datei hochzuladen.",
"dragAndDrop": "Sie können Ihre Datei auch direkt in die Dropzone ziehen."
},
"filesearch": {
"fileSearch": {
"button": {
"title": "Dateipfad Suche"
},
"file": "Datei",
"home": "Zurück zu Sources",
"input": {
"placeholder": "Dateipfad Suche",

View File

@@ -111,6 +111,7 @@
"title": "Schlüssel erzeugt",
"text1": "Ihr neuer API-Schlüssel ist bereit. Sie können diesen als Token für Zugriffe auf die REST-Schnittstelle nutzen oder anstelle Ihres Passworts zum Login mit SCM-Clients nutzen.",
"text2": "Sichern Sie Ihren API-Schlüssel jetzt! Er wird hier einmalig angezeigt und kann später nicht mehr wiederbeschafft werden.",
"alt": "Api Key",
"clipboard": "In die Zwischenablage kopieren",
"close": "Schließen"
}

View File

@@ -36,6 +36,7 @@
"installedNavLink": "Installed",
"availableNavLink": "Available"
},
"markedAsPending": "Marked as pending",
"showPending": "Show Changes",
"executePending": "Execute Changes",
"outdatedPlugins": "Update {{count}} Plugin",

View File

@@ -26,7 +26,8 @@
},
"breadcrumb": {
"home": "Main page",
"copyPermalink": "Copy Permalink to Clipboard"
"copyPermalink": "Copy Permalink to Clipboard",
"loading": "Loading ..."
},
"errorNotification": {
"prefix": "Error",
@@ -60,6 +61,10 @@
"groups": "Groups",
"admin": "Administration"
},
"secondaryNavigation": {
"showContent": "Enlarge navigation",
"hideContent": "Reduce navigation"
},
"filterEntries": "filter entries",
"autocomplete": {
"group": "Group",
@@ -134,10 +139,16 @@
"toastTitle": "Notification",
"xMore": "+{{ count }} Notification",
"xMore_plural": "+{{ count }} Notifications",
"loading": "Loading ...",
"bellTitle": "Notifications",
"empty": "No notifications",
"dismiss": "Dismiss",
"dismissAll": "Dismiss all"
},
"cardColumnGroup": {
"showContent": "Show content",
"hideContent": "Hide content"
},
"duration": {
"ms": "{{count}} millisecond",
"ms_plural": "{{count}} milliseconds",
@@ -180,6 +191,10 @@
"exampleValue": "Example Value",
"hints": "Hints"
},
"expandable": {
"showMore": "Show more information",
"hideMore": "Hide more information"
},
"exampleQueries": {
"title": "Example Queries",
"description": "Combine Fields with Modifiers and Operators to find your repositories.",
@@ -192,7 +207,7 @@
"utilities": {
"title": "Utilities",
"description": "Convert human-readable timestamps to Epoch Milliseconds to use in your query. Date is mandatory, hour, minute and seconds are optional.",
"datetime":{
"datetime": {
"label": "Convert timestamps to Epoch Milliseconds",
"format": "yyyy-mm-dd hh:mm:ss",
"convertButtonLabel": "Convert"

View File

@@ -46,7 +46,7 @@
},
"add-member-autocomplete": {
"placeholder": "Add Member",
"loading": "Loading...",
"loading": "Loading ...",
"no-options": "No suggestion available"
},
"groupForm": {

View File

@@ -240,6 +240,8 @@
},
"contributors": {
"mailto": "Send mail to",
"showList": "Show list of contributors",
"hideList": "Hide list of contributors",
"list": "List of contributors",
"authoredBy": "Authored by",
"committedBy": "committed by",
@@ -323,12 +325,15 @@
"showMarkdown": "Render markdown",
"showSources": "Show sources"
},
"showMore": "Show more information",
"hideMore": "Hide more information",
"path": "Path",
"branch": "Branch",
"commitDate": "Commit Date",
"description": "Description",
"size": "Size",
"copyPermalink": "Copy Permalink to Clipboard"
"copyPermalink": "Copy Permalink to Clipboard",
"loading": "Loading ..."
},
"noSources": "No sources found for this branch.",
"extension": {
@@ -476,16 +481,20 @@
"fullscreen": {
"open": "Open in Fullscreen",
"close": "Close"
}
},
"renamedTo": "renamed to",
"showContent": "Show diff content",
"hideContent": "Hide diff content"
},
"fileUpload": {
"clickHere": "Click here to select your file",
"dragAndDrop": "Drag 'n' drop some files here"
},
"filesearch": {
"fileSearch": {
"button": {
"title": "Search filepath"
},
"file": "File",
"home": "Go back to source root",
"input": {
"placeholder": "Search filepath",

View File

@@ -111,6 +111,7 @@
"title": "Key Created",
"text1": "Your new API key is ready. You can use it as a bearer token for REST calls or as a password for SCM clients.",
"text2": "Store your API key in a safe place now! It is only displayed now and cannot be recovered later.",
"alt": "Api Key",
"clipboard": "Copy to clipboard",
"close": "Close"
}

View File

@@ -71,7 +71,12 @@ const PluginEntry: FC<Props> = ({ plugin, openModal }) => {
};
const pendingSpinner = () => (
<Icon className="fa-spin fa-lg" name="spinner" color={plugin.markedForUninstall ? "danger" : "info"} />
<Icon
className="fa-spin fa-lg"
name="spinner"
color={plugin.markedForUninstall ? "danger" : "info"}
alt={t("plugins.markedAsPending")}
/>
);
const actionBar = () => (
<ActionbarWrapper className="is-flex">

View File

@@ -76,7 +76,7 @@ class InfoBox extends React.Component<Props> {
"is-align-items-center"
)}
>
<Icon className="has-text-blue-light mb-2 fa-2x" name={icon} color="inherit" />
<Icon className="has-text-blue-light mb-2 fa-2x" name={icon} color="inherit" alt="" />
<div className="is-size-4">{t("login." + type)}</div>
<div className="is-size-4">{t("login.tip")}</div>
</FixedSizedIconWrapper>

View File

@@ -128,7 +128,7 @@ const NotificationEntry: FC<EntryProps> = ({ notification, removeToast }) => {
</DateColumn>
<DismissColumn className="is-darker">
{isLoading ? (
<div className="small-loading-spinner" />
<div className="small-loading-spinner" aria-label={t("notifications.loading")} />
) : (
<Icon
name="trash"
@@ -165,7 +165,7 @@ const ClearEntry: FC<ClearEntryProps> = ({ notifications, clearToasts }) => {
<div className={classNames("dropdown-item", "has-text-centered")}>
<ErrorNotification error={error} />
<DismissAllButton className="is-outlined" color="link" loading={isLoading} action={clear}>
<Icon color="link" name="trash" className="mr-1" /> {t("notifications.dismissAll")}
<Icon color="link" name="trash" className="mr-1" alt="" /> {t("notifications.dismissAll")}
</DismissAllButton>
</div>
);
@@ -292,13 +292,20 @@ type BellNotificationIconProps = {
};
const BellNotificationIcon: FC<BellNotificationIconProps> = ({ data, onClick }) => {
const [t] = useTranslation("commons");
const counter = data?._embedded.notifications.length || 0;
return (
<BellNotificationContainer
className={classNames("is-relative", "is-flex", "is-justify-content-center", "is-align-items-center")}
onClick={onClick}
>
<Icon className="is-size-4" iconStyle={counter === 0 ? "far" : "fas"} name="bell" color="white" />
<Icon
className="is-size-4"
iconStyle={counter === 0 ? "far" : "fas"}
name="bell"
color="white"
alt={t("notifications.bellTitle")}
/>
{counter > 0 ? <NotificationCounter count={counter}>{counter < 100 ? counter : "∞"}</NotificationCounter> : null}
</BellNotificationContainer>
);

View File

@@ -41,7 +41,7 @@ export default class GroupMember extends React.Component<Props> {
renderLink(to: string, label: string) {
return (
<Link to={to}>
<Icon name="user" color="inherit" /> {label}
<Icon name="user" color="inherit" alt="" /> {label}
</Link>
);
}

View File

@@ -40,7 +40,7 @@ const FileSearchButton: FC<Props> = ({ baseUrl, revision }) => {
const [t] = useTranslation("repos");
return (
<Link to={`${baseUrl}/search/${encodeURIComponent(revision)}`}>
<SearchIcon title={t("filesearch.button.title")} name="search" color="inherit" />
<SearchIcon title={t("fileSearch.button.title")} name="search" color="inherit" />
</Link>
);
};

View File

@@ -52,12 +52,13 @@ type PathResultRowProps = {
};
const PathResultRow: FC<PathResultRowProps> = ({ contentBaseUrl, path }) => {
const [t] = useTranslation("repos");
const link = urls.concat(contentBaseUrl, path);
return (
<tr>
<IconColumn>
<Link to={link}>
<Icon title="File" name="file" color="inherit" />
<Icon title={t("fileSearch.file")} name="file" color="inherit" />
</Link>
</IconColumn>
<LeftOverflowTd>
@@ -90,14 +91,14 @@ const FileSearchResults: FC<Props> = ({ query, contentBaseUrl, paths = [] }) =>
if (query.length <= 1) {
body = (
<Notification className="m-4" type="info">
{t("filesearch.notifications.queryToShort")}
{t("fileSearch.notifications.queryToShort")}
</Notification>
);
} else if (paths.length === 0) {
const queryCmp = <strong>{query}</strong>;
body = (
<Notification className="m-4" type="info">
<Trans i18nKey="repos:filesearch.notifications.emptyResult" values={{ query }} components={[queryCmp]} />
<Trans i18nKey="repos:fileSearch.notifications.emptyResult" values={{ query }} components={[queryCmp]} />
</Notification>
);
} else {

View File

@@ -113,16 +113,16 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
)}
>
<HomeLink className={classNames("mr-3", "pr-3")} to={contentBaseUrl}>
<HomeIcon title={t("filesearch.home")} name="home" color="inherit" />
<HomeIcon title={t("fileSearch.home")} name="home" color="inherit" />
</HomeLink>
<FilterInput
className="is-full-width"
placeholder={t("filesearch.input.placeholder")}
placeholder={t("fileSearch.input.placeholder")}
value={query}
filter={search}
autoFocus={true}
/>
<Help className="ml-3" message={t("filesearch.input.help")} />
<Help className="ml-3" message={t("fileSearch.input.help")} />
</div>
<ErrorNotification error={error} />
{isLoading ? <Loading /> : <FileSearchResults contentBaseUrl={contentBaseUrl} query={query} paths={result} />}

View File

@@ -93,7 +93,7 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
<div className="is-flex is-flex-direction-column mb-4">
<div className="is-flex">
<p className="is-ellipsis-overflow is-clickable mb-2" onClick={(e) => setOpen(!open)}>
<Icon name="angle-down" /> {t("changeset.contributors.list")}
<Icon name="angle-down" alt={t("changeset.contributors.hideList")} /> {t("changeset.contributors.list")}
</p>
{signatureIcon}
</div>
@@ -106,7 +106,8 @@ const Contributors: FC<{ changeset: Changeset }> = ({ changeset }) => {
<>
<div className="is-flex is-clickable" onClick={(e) => setOpen(!open)}>
<ContributorColumn className="is-ellipsis-overflow">
<Icon name="angle-right" /> <ChangesetAuthor changeset={changeset} />
<Icon name="angle-right" alt={t("changeset.contributors.showList")} />{" "}
<ChangesetAuthor changeset={changeset} />
</ContributorColumn>
{signatureIcon}
<CountColumn className="is-hidden-mobile is-hidden-tablet-only is-hidden-desktop-only">
@@ -229,6 +230,7 @@ const ChangesetDetails: FC<Props> = ({ changeset, repository, fileControlFactory
color="default"
icon={collapsed ? "eye" : "eye-slash"}
label={t("changesets.collapseDiffs")}
title={t("changesets.collapseDiffs")}
reducedMobile={true}
/>
}

View File

@@ -57,7 +57,7 @@ const RepositoryFormButton: FC<RepositoryForm> = ({ path, icon, label }) => {
color={isSelected ? "link is-selected" : undefined}
link={!isSelected ? href : undefined}
>
<Icon className="pr-2" name={icon} color={isSelected ? "white" : "default"} />
<Icon className="pr-2" name={icon} color={isSelected ? "white" : "default"} alt="" />
<p className={classNames("is-hidden-mobile", "is-hidden-tablet-only")}>{t(`plugins:${label}`, label)}</p>
</Button>
);

View File

@@ -36,7 +36,7 @@ const RepositoryGroupEntry: FC<Props> = ({ group }) => {
const settingsLink = group.namespace?._links?.permissions && (
<Link to={`/namespace/${group.name}/settings`}>
<Icon color="is-link" name="cog" title={t("repositoryOverview.settings.tooltip")} className="is-size-6 ml-2" />
<Icon color="inherit" name="cog" title={t("repositoryOverview.settings.tooltip")} className="is-size-6 ml-2" />
</Link>
);
const namespaceHeader = (

View File

@@ -24,7 +24,7 @@
import React, { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Namespace, Permission, Repository } from "@scm-manager/ui-types";
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
import { ConfirmAlert, ErrorNotification, Icon } from "@scm-manager/ui-components";
import { useDeletePermission } from "@scm-manager/ui-api";
type Props = {
@@ -63,12 +63,12 @@ const DeletePermissionButton: FC<Props> = ({ namespaceOrRepository, permission,
className: "is-outlined",
label: t("permission.delete-permission-button.confirm-alert.submit"),
isLoading,
onClick: () => deletePermission()
onClick: () => deletePermission(),
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
onClick: () => null,
},
]}
close={() => setShowConfirmAlert(false)}
/>
@@ -80,7 +80,7 @@ const DeletePermissionButton: FC<Props> = ({ namespaceOrRepository, permission,
<ErrorNotification error={error} />
<a className="level-item" onClick={action}>
<span className="icon is-small">
<i className="fas fa-trash" />
<Icon name="trash" title={t("permission.delete-permission-button.label")} color="inherit" />
</span>
</a>
</>

View File

@@ -78,13 +78,13 @@ class FileTreeLeaf extends React.Component<Props> {
} else if (file.computationAborted) {
return (
<Tooltip location="top" message={t("sources.fileTree.computationAborted")}>
<Icon name="question-circle" />
<Icon name="question-circle" alt={t("sources.fileTree.computationAborted")} />
</Tooltip>
);
} else if (file.partialResult) {
return (
<Tooltip location="top" message={t("sources.fileTree.notYetComputed")}>
<Icon name="hourglass" />
<Icon name="hourglass" alt={t("sources.fileTree.notYetComputed")} />
</Tooltip>
);
} else {

View File

@@ -56,27 +56,33 @@ class FileButtonAddons extends React.Component<Props> {
return (
<ButtonAddons className={className}>
<div title={t("sources.content.sourcesButton")}>
<Button action={showSources} color={this.color(selected === "source")}>
<span className="icon">
<i className="fas fa-code" />
</span>
</Button>
</div>
<div title={t("sources.content.annotateButton")}>
<Button action={showAnnotations} color={this.color(selected === "annotations")}>
<span className="icon">
<i className="fas fa-user-clock" />
</span>
</Button>
</div>
<div title={t("sources.content.historyButton")}>
<Button action={showHistory} color={this.color(selected === "history")}>
<span className="icon">
<i className="fas fa-history" />
</span>
</Button>
</div>
<Button
action={showSources}
color={this.color(selected === "source")}
title={t("sources.content.sourcesButton")}
>
<span className="icon">
<i className="fas fa-code" />
</span>
</Button>
<Button
action={showAnnotations}
color={this.color(selected === "annotations")}
title={t("sources.content.annotateButton")}
>
<span className="icon">
<i className="fas fa-user-clock" />
</span>
</Button>
<Button
action={showHistory}
color={this.color(selected === "history")}
title={t("sources.content.historyButton")}
>
<span className="icon">
<i className="fas fa-history" />
</span>
</Button>
</ButtonAddons>
);
}

View File

@@ -88,7 +88,26 @@ const Content: FC<Props> = ({ file, repository, revision, breadcrumb, error }) =
};
const showHeader = (content: ReactNode) => {
const icon = collapsed ? "angle-right" : "angle-down";
let icon;
if (collapsed) {
icon = (
<Icon
className={classNames("is-inline", "mr-2")}
name="angle-right fa-fw"
color="inherit"
alt={t("sources.content.showMore")}
/>
);
} else {
icon = (
<Icon
className={classNames("is-inline", "mr-2")}
name="angle-down fa-fw"
color="inherit"
alt={t("sources.content.hideMore")}
/>
);
}
const selector = file._links.history ? (
<FileButtonAddons
@@ -107,7 +126,7 @@ const Content: FC<Props> = ({ file, repository, revision, breadcrumb, error }) =
className={classNames("level-left", "is-flex", "is-clickable", "is-word-break", "mr-2")}
onClick={toggleCollapse}
>
<Icon className={classNames("is-inline", "mr-2")} name={`${icon} fa-fw`} color="inherit" />
{icon}
{file.name}
</FullWidthTitleHeader>
<div className={classNames("level-right", "buttons", "ml-auto")}>

View File

@@ -50,13 +50,18 @@ type ExpandableProps = {
};
const Expandable: FC<ExpandableProps> = ({ header, children, className }) => {
const [t] = useTranslation("commons");
const [expanded, setExpanded] = useState(false);
return (
<div className={classNames("card", className)}>
<header onClick={() => setExpanded(!expanded)} className="card-header is-clickable">
<span className="card-header-title">{header}</span>
<span className="card-header-icon">
<Icon name={expanded ? "chevron-down" : "chevron-left"} />
{expanded ? (
<Icon name="chevron-down" alt={t("search.syntax.expandable.hideMore")} />
) : (
<Icon name="chevron-left" alt={t("search.syntax.expandable.showMore")} />
)}
</span>
</header>
{expanded ? <div className="card-content">{children}</div> : null}
@@ -201,7 +206,13 @@ const TimestampConverter: FC = () => {
{copying ? (
<span className="small-loading-spinner" />
) : (
<Icon name="clipboard" color="inherit" className="is-size-4 fa-fw is-clickable" onClick={copyTimestamp} />
<Icon
name="clipboard"
color="inherit"
className="is-size-4 fa-fw is-clickable"
onClick={copyTimestamp}
alt={t("search.syntax.utilities.copyTimestampTooltip")}
/>
)}
</StyledTooltip>
</span>

View File

@@ -35,7 +35,8 @@ type Props = {
const KeyArea = styled.textarea`
white-space: nowrap;
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
font-family: "Courier New", Monaco, Menlo, "Ubuntu Mono", "source-code-pro", monospace;
height: 3rem;
`;
@@ -64,7 +65,13 @@ const ApiKeyCreatedModal: FC<Props> = ({ addedKey, close }) => {
<hr />
<div className="columns">
<div className="column is-11">
<KeyArea wrap={"soft"} ref={keyRef} className="input" value={addedKey.token} />
<KeyArea
wrap={"soft"}
ref={keyRef}
className="input"
value={addedKey.token}
aria-label={t("apiKey.modal.alt")}
/>
</div>
<NoLeftMargin className="column is-1">
<Icon