Add keyboard navigation for users, groups, branches, tags, sources, changesets and plugins (#2153)

Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2022-11-10 11:44:53 +01:00
committed by GitHub
parent da71004dd0
commit eea60deadb
28 changed files with 819 additions and 373 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Keyboard navigation for users, groups, branches, tags, sources, changesets and plugins ([#2153](https://github.com/scm-manager/scm-manager/pull/2153))

View File

@@ -21,6 +21,7 @@
},
"devDependencies": {
"@scm-manager/ui-syntaxhighlighting": "2.39.2-SNAPSHOT",
"@scm-manager/ui-shortcuts": "2.39.2-SNAPSHOT",
"@scm-manager/ui-text": "2.39.2-SNAPSHOT",
"@scm-manager/babel-preset": "^2.13.1",
"@scm-manager/eslint-config": "^2.16.0",

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, ReactNode } from "react";
import React, { ReactNode, useCallback } from "react";
import classNames from "classnames";
import styled from "styled-components";
import { Link } from "react-router-dom";
@@ -55,27 +55,29 @@ const InvisibleButton = styled.button`
cursor: pointer;
`;
const CardColumn: FC<Props> = ({
link,
avatar,
title,
description,
contentRight,
footerLeft,
footerRight,
action,
className,
}) => {
const CardColumn = React.forwardRef<HTMLElement, Props>(
({ link, avatar, title, description, contentRight, footerLeft, footerRight, action, className }, ref) => {
const renderAvatar = avatar ? <figure className="media-left mt-3 ml-4">{avatar}</figure> : null;
const renderDescription = description ? <p className="shorten-text">{description}</p> : null;
const renderContentRight = contentRight ? <div className="ml-auto">{contentRight}</div> : null;
const executeRef = useCallback(
(el: HTMLButtonElement | HTMLAnchorElement | null) => {
if (typeof ref === "function") {
ref(el);
} else if (ref) {
ref.current = el;
}
},
[ref]
);
let createLink = null;
if (link) {
createLink = <Link className="overlay-column" to={link} />;
createLink = <Link ref={executeRef} className="overlay-column" to={link} />;
} else if (action) {
createLink = (
<InvisibleButton
ref={executeRef}
className="overlay-column"
onClick={(e) => {
e.preventDefault();
@@ -118,6 +120,7 @@ const CardColumn: FC<Props> = ({
</NoEventWrapper>
</>
);
};
}
);
export default CardColumn;

View File

@@ -41,13 +41,13 @@ class CardColumnGroup extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
collapsed: false
collapsed: false,
};
}
toggleCollapse = () => {
this.setState(prevState => ({
collapsed: !prevState.collapsed
this.setState((prevState) => ({
collapsed: !prevState.collapsed,
}));
};
@@ -75,7 +75,10 @@ class CardColumnGroup extends React.Component<Props, State> {
const fullColumnWidth = this.isFullSize(elements, index);
const sizeClass = fullColumnWidth ? "is-full" : "is-half";
return (
<div className={classNames("box", "box-link-shadow", "column", "is-clipped", sizeClass)} key={index}>
<div
className={classNames("box", "box-link-shadow", "column", "is-relative", "is-clipped", sizeClass)}
key={index}
>
{entry}
</div>
);

View File

@@ -934,6 +934,24 @@ exports[`Storyshots Buttons/Button Loading 1`] = `
</div>
`;
exports[`Storyshots Buttons/Button Ref Default 1`] = `
<div
className="indexstories__Spacing-sc-bpoict-0 jXUBTh"
>
<button
className="button is-default"
onClick={[Function]}
type="button"
>
<span>
Focus me!
</span>
</button>
</div>
`;
exports[`Storyshots Buttons/CreateButton Default 1`] = `
<div
className="indexstories__Spacing-sc-bpoict-0 jXUBTh"
@@ -20096,7 +20114,7 @@ exports[`Storyshots Repositories/Changesets Co-Authors with avatar 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
onClick={[Function]}
>
@@ -20116,7 +20134,7 @@ exports[`Storyshots Repositories/Changesets Co-Authors with avatar 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
onClick={[Function]}
>
@@ -20253,7 +20271,7 @@ exports[`Storyshots Repositories/Changesets Commiter and Co-Authors with avatar
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
@@ -20273,7 +20291,7 @@ exports[`Storyshots Repositories/Changesets Commiter and Co-Authors with avatar
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
@@ -20368,7 +20386,7 @@ exports[`Storyshots Repositories/Changesets Default 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -20388,7 +20406,371 @@ exports[`Storyshots Repositories/Changesets Default 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.sources
</span>
</a>
</p>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Storyshots Repositories/Changesets List with navigation 1`] = `
<div
className="Changesetsstories__Wrapper-sc-122npan-0 bEvXuM box box-link-shadow"
>
<div
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
>
<div
className="columns is-variable is-1-mobile is-0-tablet"
>
<div
className="column is-three-fifths is-full-mobile"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
The starship Heart of Gold was the first spacecraft to make use of the Infinite Improbability Drive. The craft was stolen by then-President Zaphod Beeblebrox at the official launch of the ship, as he was supposed to be officiating the launch. Later, during the use of the Infinite Improbability Drive, the ship picked up Arthur Dent and Ford Prefect, who were floating unprotected in deep space in the same star sector, having just escaped the destruction of the same planet.
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<div
className="is-flex"
>
<p
className="is-size-7 is-ellipsis-overflow mt-2"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
,
changeset.contributors.committedBy
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
Zaphod Beeblebrox
</a>
commaSeparatedList.lastDivider
changeset.contributors.coAuthoredBy
<a
href="mailto:ford.prefect@hitchhiker.com"
title="changeset.contributors.mailto ford.prefect@hitchhiker.com"
>
Ford Prefect
</a>
</p>
</div>
</div>
</div>
</div>
<div
className="column is-align-self-center"
/>
</div>
</div>
<div
className="column is-flex is-justify-content-flex-end is-align-items-center"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
>
<p
className="control"
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.details
</span>
</a>
</p>
<p
className="control"
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.sources
</span>
</a>
</p>
</div>
</div>
</div>
</div>
<div
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
>
<div
className="columns is-variable is-1-mobile is-0-tablet"
>
<div
className="column is-three-fifths is-full-mobile"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
Change heading to "Heart Of Gold"
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<div
className="is-flex"
>
<p
className="is-size-7 is-ellipsis-overflow mt-2"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
commaSeparatedList.lastDivider
changeset.contributors.committedBy
<a
href="mailto:zaphod.beeblebrox@hitchhiker.cm"
title="changeset.contributors.mailto zaphod.beeblebrox@hitchhiker.cm"
>
Zaphod Beeblebrox
</a>
</p>
</div>
</div>
</div>
</div>
<div
className="column is-align-self-center"
/>
</div>
</div>
<div
className="column is-flex is-justify-content-flex-end is-align-items-center"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
>
<p
className="control"
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.details
</span>
</a>
</p>
<p
className="control"
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-code has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.sources
</span>
</a>
</p>
</div>
</div>
</div>
</div>
<div
className="ChangesetRow__Wrapper-sc-tkpti5-0 fyJvMU"
>
<div
className="columns is-variable is-1-mobile is-0-tablet"
>
<div
className="column is-three-fifths is-full-mobile"
>
<div
className="columns is-gapless"
>
<div
className="column is-four-fifths"
>
<div
className="media"
>
<div
className="SingleChangeset__FullWidthDiv-sc-ytpqp9-1 gydPEK media-right ml-0"
>
<h4
className="has-text-weight-bold is-ellipsis-overflow"
>
initialize repository
</h4>
<p
className="is-hidden-touch"
/>
<p
className="is-hidden-desktop"
/>
<div
className="is-flex"
>
<p
className="is-size-7 is-ellipsis-overflow mt-2"
>
changeset.contributors.authoredBy
<a
href="mailto:scm-admin@scm-manager.org"
title="changeset.contributors.mailto scm-admin@scm-manager.org"
>
SCM Administrator
</a>
</p>
</div>
</div>
</div>
</div>
<div
className="column is-align-self-center"
/>
</div>
</div>
<div
className="column is-flex is-justify-content-flex-end is-align-items-center"
>
<div
className="ButtonAddons__Flex-sc-182golj-0 dcwwZy field has-addons m-0"
>
<p
className="control"
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
<span>
<i
className="fas fa-fw fa-exchange-alt has-text-inherit is-medium pr-5"
onKeyPress={[Function]}
/>
changeset.buttons.details
</span>
</a>
</p>
<p
className="control"
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -20493,7 +20875,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
@@ -20513,7 +20895,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
@@ -20620,7 +21002,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
@@ -20640,7 +21022,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/d21cc6c359270aef2196796f4d96af65f51866dc"
onClick={[Function]}
>
@@ -20756,7 +21138,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
@@ -20776,7 +21158,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/a88567ef1e9528a700555cad8c4576b72fc7c6dd"
onClick={[Function]}
>
@@ -20884,7 +21266,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -20904,7 +21286,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21008,7 +21390,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21028,7 +21410,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21132,7 +21514,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21152,7 +21534,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21260,7 +21642,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
onClick={[Function]}
>
@@ -21280,7 +21662,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/b6c6f8fbd0d490936fae7d26ffdd4695cc2a0930"
onClick={[Function]}
>
@@ -21384,7 +21766,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21404,7 +21786,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21508,7 +21890,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21528,7 +21910,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21632,7 +22014,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21652,7 +22034,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21756,7 +22138,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21776,7 +22158,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21880,7 +22262,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -21900,7 +22282,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -22004,7 +22386,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = `
>
<a
aria-label="changeset.buttons.details"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/changeset/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>
@@ -22024,7 +22406,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = `
>
<a
aria-label="changeset.buttons.sources"
className="button is-default is-reduced-mobile ChangesetButtonGroup__SwitcherButton-sc-19ag5s8-0 GkUQw"
className="button is-default is-reduced-mobile px-3"
href="/repo/hitchhiker/heartOfGold/code/sources/e163c8f632db571c9aa51a8eb440e37cf550b825"
onClick={[Function]}
>

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, MouseEvent, ReactNode, KeyboardEvent } from "react";
import React, { KeyboardEvent, MouseEvent, ReactNode, useCallback } from "react";
import classNames from "classnames";
import { Link } from "react-router-dom";
import Icon from "../Icon";
@@ -40,7 +40,6 @@ export type ButtonProps = {
reducedMobile?: boolean;
children?: ReactNode;
testId?: string;
ref?: React.ForwardedRef<HTMLButtonElement>;
};
type Props = ButtonProps & {
@@ -48,11 +47,9 @@ type Props = ButtonProps & {
color?: string;
};
type InnerProps = Props & {
innerRef: React.Ref<HTMLButtonElement>;
};
const Button: FC<InnerProps> = ({
const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
(
{
link,
className,
icon,
@@ -67,8 +64,19 @@ const Button: FC<InnerProps> = ({
disabled,
action,
color = "default",
innerRef
}) => {
},
ref
) => {
const executeRef = useCallback(
(el: HTMLButtonElement | HTMLAnchorElement | null) => {
if (typeof ref === "function") {
ref(el);
} else if (ref) {
ref.current = el;
}
},
[ref]
);
const renderIcon = () => {
return (
<>
@@ -102,13 +110,13 @@ const Button: FC<InnerProps> = ({
if (link && !disabled) {
if (link.includes("://")) {
return (
<a className={classes} href={link} aria-label={label}>
<a ref={executeRef} className={classes} href={link} aria-label={label}>
{content}
</a>
);
}
return (
<Link className={classes} to={link} aria-label={label}>
<Link ref={executeRef} className={classes} to={link} aria-label={label}>
{content}
</Link>
);
@@ -119,14 +127,15 @@ const Button: FC<InnerProps> = ({
type={type}
title={title}
disabled={disabled}
onClick={event => action && action(event)}
onClick={(event) => action && action(event)}
className={classes}
ref={innerRef}
ref={executeRef}
{...createAttributesForTesting(testId)}
>
{content}
</button>
);
};
}
);
export default React.forwardRef<HTMLButtonElement, Props>((props, ref) => <Button {...props} innerRef={ref} />);
export default Button;

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { ReactElement, ReactNode } from "react";
import React, { ReactElement, ReactNode, useEffect, useState } from "react";
import Button from "./Button";
import { storiesOf } from "@storybook/react";
import styled from "styled-components";
@@ -85,3 +85,8 @@ buttonStory("DownloadButton", () => <DownloadButton displayName="Download" disab
);
buttonStory("EditButton", () => <EditButton>Edit</EditButton>);
buttonStory("SubmitButton", () => <SubmitButton>Submit</SubmitButton>);
buttonStory("Button Ref", () => {
const [ref, setRef] = useState<HTMLButtonElement | HTMLAnchorElement | null>();
useEffect(() => ref?.focus(), [ref]);
return <Button ref={setRef}>Focus me!</Button>;
});

View File

@@ -37,16 +37,22 @@ export default function useRegisterModal(active: boolean, initialValue: boolean
useEffect(() => {
if (active) {
previousActiveState.current = true;
if (increment) {
increment();
}
} else {
if (previousActiveState.current !== null) {
if (decrement) {
decrement();
}
}
previousActiveState.current = false;
}
return () => {
if (previousActiveState.current) {
if (decrement) {
decrement();
}
previousActiveState.current = null;
}
};

View File

@@ -21,12 +21,11 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC } from "react";
import React from "react";
import { Changeset, File, Repository } from "@scm-manager/ui-types";
import { Button, ButtonAddons } from "../../buttons";
import { createChangesetLink, createSourcesLink } from "./changesets";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
type Props = {
repository: Repository;
@@ -34,26 +33,31 @@ type Props = {
file?: File;
};
const SwitcherButton = styled(Button)`
padding-right: 0.75rem;
padding-left: 0.75rem;
`;
const ChangesetButtonGroup: FC<Props> = ({ repository, changeset, file }) => {
const ChangesetButtonGroup = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>(
({ repository, changeset, file }, ref) => {
const [t] = useTranslation("repos");
const changesetLink = createChangesetLink(repository, changeset);
const sourcesLink = createSourcesLink(repository, changeset, file);
return (
<ButtonAddons className="m-0">
<SwitcherButton
<Button
className="px-3"
ref={ref}
link={changesetLink}
icon="exchange-alt"
label={t("changeset.buttons.details")}
reducedMobile={true}
/>
<SwitcherButton link={sourcesLink} icon="code" label={t("changeset.buttons.sources")} reducedMobile={true} />
<Button
className="px-3"
link={sourcesLink}
icon="code"
label={t("changeset.buttons.sources")}
reducedMobile={true}
/>
</ButtonAddons>
);
};
}
);
export default ChangesetButtonGroup;

View File

@@ -24,6 +24,7 @@
import ChangesetRow from "./ChangesetRow";
import React, { FC } from "react";
import { Changeset, File, Repository } from "@scm-manager/ui-types";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -32,10 +33,13 @@ type Props = {
};
const ChangesetList: FC<Props> = ({ repository, changesets, file }) => {
const content = changesets.map(changeset => {
return (
<KeyboardIterator>
{changesets.map((changeset) => {
return <ChangesetRow key={changeset.id} repository={repository} changeset={changeset} file={file} />;
});
return <>{content}</>;
})}
</KeyboardIterator>
);
};
export default ChangesetList;

View File

@@ -28,6 +28,7 @@ import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { Changeset, File, Repository } from "@scm-manager/ui-types";
import ChangesetButtonGroup from "./ChangesetButtonGroup";
import SingleChangeset from "./SingleChangeset";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -46,6 +47,7 @@ const Wrapper = styled.div`
`;
const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
const ref = useKeyboardIteratorTarget();
return (
<Wrapper>
<div className={classNames("columns", "is-variable", "is-1-mobile", "is-0-tablet")}>
@@ -53,12 +55,12 @@ const ChangesetRow: FC<Props> = ({ repository, changeset, file }) => {
<SingleChangeset repository={repository} changeset={changeset} />
</div>
<div className={classNames("column", "is-flex", "is-justify-content-flex-end", "is-align-items-center")}>
<ChangesetButtonGroup repository={repository} changeset={changeset} file={file} />
<ChangesetButtonGroup ref={ref} repository={repository} changeset={changeset} file={file} />
<ExtensionPoint<extensionPoints.ChangesetRight>
name="changeset.right"
props={{
repository,
changeset
changeset,
}}
renderAll={true}
/>

View File

@@ -28,13 +28,15 @@ import styled from "styled-components";
import { MemoryRouter } from "react-router-dom";
import repository from "../../__resources__/repository";
import ChangesetRow from "./ChangesetRow";
import { one, two, three, four, five } from "../../__resources__/changesets";
import { five, four, one, three, two } from "../../__resources__/changesets";
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
// @ts-ignore
import hitchhiker from "../../__resources__/hitchhiker.png";
import { Person } from "../../avatar/Avatar";
import { Changeset } from "@scm-manager/ui-types";
import { Replacement } from "@scm-manager/ui-text";
import ChangesetList from "./ChangesetList";
import { ShortcutDocsContextProvider } from "@scm-manager/ui-shortcuts";
const Wrapper = styled.div`
margin: 25rem 4rem;
@@ -287,4 +289,9 @@ storiesOf("Repositories/Changesets", module)
},
];
return <ChangesetRow repository={repository} changeset={changeset} />;
});
})
.add("List with navigation", () => (
<ShortcutDocsContextProvider>
<ChangesetList repository={repository} changesets={[copy(one), copy(two), copy(three)]} />
</ShortcutDocsContextProvider>
));

View File

@@ -88,7 +88,7 @@ describe("shortcutIterator", () => {
expect(result.error).toBeUndefined();
});
it("should call last callback upon pressing forward in initial state", async () => {
it("should call first callback upon pressing forward in initial state", async () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
@@ -101,12 +101,12 @@ describe("shortcutIterator", () => {
Mousetrap.trigger("j");
expect(callback).not.toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
expect(callback3).toHaveBeenCalledTimes(1);
expect(callback3).not.toHaveBeenCalled();
});
it("should call first callback once upon pressing backward in initial state", async () => {
it("should call last callback once upon pressing backward in initial state", async () => {
const callback = jest.fn();
const callback2 = jest.fn();
const callback3 = jest.fn();
@@ -117,12 +117,11 @@ describe("shortcutIterator", () => {
</Wrapper>
);
Mousetrap.trigger("k");
Mousetrap.trigger("k");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback3).toHaveBeenCalledTimes(1);
});
it("should not allow moving past the end of the callback array", async () => {

View File

@@ -57,7 +57,7 @@ export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({
const navigateBackward = useCallback(() => {
if (activeIndex.current === -1) {
activeIndex.current = 0;
activeIndex.current = callbacks.current.length - 1;
executeCallback(activeIndex.current);
} else if (activeIndex.current > 0) {
activeIndex.current -= 1;
@@ -67,7 +67,7 @@ export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({
const navigateForward = useCallback(() => {
if (activeIndex.current === -1) {
activeIndex.current = callbacks.current.length - 1;
activeIndex.current = 0;
executeCallback(activeIndex.current);
} else if (activeIndex.current < callbacks.current.length - 1) {
activeIndex.current += 1;

View File

@@ -537,19 +537,11 @@ ul.is-separated {
}
.content.is-plugin-page {
.card-columns {
.column.is-half .overlay-column {
width: calc(37.5% - 1.5rem);
}
.column.is-full .overlay-column {
width: calc(75% - 1.5rem);
}
@media screen and (max-width: 768px) {
.column.is-half .overlay-column,
.column.is-full .overlay-column {
width: calc(100% - 1.5rem);
}
.column .overlay-column {
left: 0.5rem;
top: 0.5rem;
width: calc(100% - 1rem);
height: calc(160px - 1rem);
}
}
}

View File

@@ -30,6 +30,7 @@ import { useTranslation } from "react-i18next";
import PluginAvatar from "./PluginAvatar";
import classNames from "classnames";
import MyCloudoguTag from "./MyCloudoguTag";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = {
plugin: Plugin;
@@ -43,8 +44,8 @@ const ActionbarWrapper = styled.div`
}
`;
const IconWrapperStyle = styled.span.attrs(props => ({
className: "level-item mb-0 p-2 is-clickable"
const IconWrapperStyle = styled.span.attrs((props) => ({
className: "level-item mb-0 p-2 is-clickable",
}))`
border: 1px solid #cdcdcd; // $dark-25
border-radius: 4px;
@@ -56,7 +57,7 @@ const IconWrapperStyle = styled.span.attrs(props => ({
const IconWrapper: FC<{ action: () => void }> = ({ action, children }) => {
return (
<IconWrapperStyle onClick={action} onKeyDown={e => e.key === "Enter" && action()} tabIndex={0}>
<IconWrapperStyle onClick={action} onKeyDown={(e) => e.key === "Enter" && action()} tabIndex={0}>
{children}
</IconWrapperStyle>
);
@@ -69,6 +70,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
const isUninstallable = plugin._links.uninstall && (plugin._links.uninstall as Link).href;
const isCloudoguPlugin = plugin.type === "CLOUDOGU";
const isDefaultPluginCenterLoginAvailable = pluginCenterAuthInfo?.default && !!pluginCenterAuthInfo?._links?.login;
const ref = useKeyboardIteratorTarget();
const evaluateAction = () => {
if (isInstallable) {
@@ -118,6 +120,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
return (
<>
<CardColumn
ref={ref}
action={evaluateAction()}
avatar={<PluginAvatar plugin={plugin} />}
title={plugin.displayName ? <strong>{plugin.displayName}</strong> : <strong>{plugin.name}</strong>}
@@ -129,7 +132,7 @@ const PluginEntry: FC<Props> = ({ plugin, openModal, pluginCenterAuthInfo }) =>
<div
className={classNames("is-flex", {
"is-justify-content-space-between": isCloudoguPlugin,
"is-justify-content-end": !isCloudoguPlugin
"is-justify-content-end": !isCloudoguPlugin,
})}
>
{isCloudoguPlugin ? <MyCloudoguTag /> : null}

View File

@@ -26,6 +26,7 @@ import { Plugin, PluginCenterAuthenticationInfo } from "@scm-manager/ui-types";
import PluginGroupEntry from "../components/PluginGroupEntry";
import groupByCategory from "./groupByCategory";
import { PluginModalContent } from "../containers/PluginsOverview";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
plugins: Plugin[];
@@ -37,7 +38,8 @@ const PluginList: FC<Props> = ({ plugins, openModal, pluginCenterAuthInfo }) =>
const groups = groupByCategory(plugins);
return (
<div className="content is-plugin-page">
{groups.map(group => {
<KeyboardIterator>
{groups.map((group) => {
return (
<PluginGroupEntry
group={group}
@@ -47,6 +49,7 @@ const PluginList: FC<Props> = ({ plugins, openModal, pluginCenterAuthInfo }) =>
/>
);
})}
</KeyboardIterator>
</div>
);
};

View File

@@ -21,24 +21,21 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Group } from "@scm-manager/ui-types";
import { Icon } from "@scm-manager/ui-components";
import classNames from "classnames";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = WithTranslation & {
type Props = {
group: Group;
};
class GroupRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
}
render() {
const { group, t } = this.props;
const GroupRow: FC<Props> = ({ group }) => {
const ref = useKeyboardIteratorTarget();
const [t] = useTranslation("groups");
const to = `/group/${group.name}`;
const iconType = group.external ? (
<Icon title={t("group.external")} name="globe-americas" />
@@ -49,12 +46,16 @@ class GroupRow extends React.Component<Props> {
return (
<tr>
<td className="is-word-break">
{iconType} {this.renderLink(to, group.name)}
{iconType}{" "}
{
<Link ref={ref} to={to}>
{group.name}
</Link>
}
</td>
<td className={classNames("is-hidden-mobile", "is-word-break")}>{group.description}</td>
</tr>
);
}
}
};
export default withTranslation("groups")(GroupRow);
export default GroupRow;

View File

@@ -25,6 +25,7 @@ import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Group } from "@scm-manager/ui-types";
import GroupRow from "./GroupRow";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
groups: Group[];
@@ -34,6 +35,7 @@ const GroupTable: FC<Props> = ({ groups }) => {
const [t] = useTranslation("groups");
return (
<KeyboardIterator>
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
@@ -47,6 +49,7 @@ const GroupTable: FC<Props> = ({ groups }) => {
})}
</tbody>
</table>
</KeyboardIterator>
);
};

View File

@@ -32,6 +32,7 @@ import { binder } from "@scm-manager/ui-extensions";
import DefaultBranchTag from "./DefaultBranchTag";
import AheadBehindTag from "./AheadBehindTag";
import BranchCommitDateCommitter from "./BranchCommitDateCommitter";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -64,6 +65,7 @@ const MobileFlowSpan = styled.span`
const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }) => {
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
const [t] = useTranslation("repos");
const ref = useKeyboardIteratorTarget();
let deleteButton;
if ((branch?._links?.delete as Link)?.href) {
@@ -92,7 +94,7 @@ const BranchRow: FC<Props> = ({ repository, baseUrl, branch, onDelete, details }
return (
<AdaptTableFlow>
<td className="is-vertical-align-middle">
<ReactLink to={to} title={branch.name}>
<ReactLink ref={ref} to={to} title={branch.name}>
{branch.name}
</ReactLink>
{branch.lastCommitDate && (

View File

@@ -29,6 +29,7 @@ import { orderBranches } from "../util/orderBranches";
import BranchTable from "../components/BranchTable";
import { useTranslation } from "react-i18next";
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -40,11 +41,11 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
const [t] = useTranslation("repos");
const branches: Branch[] = (data?._embedded?.branches as Branch[]) || [];
orderBranches(branches);
const staleBranches = branches.filter(b => b.stale);
const activeBranches = branches.filter(b => !b.stale);
const staleBranches = branches.filter((b) => b.stale);
const activeBranches = branches.filter((b) => !b.stale);
const { error, data: branchesDetails } = useBranchDetailsCollection(repository, [
...activeBranches,
...staleBranches
...staleBranches,
]);
if (branches.length === 0) {
@@ -54,7 +55,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
const showCreateButton = !!data._links.create;
return (
<>
<KeyboardIterator>
<Subtitle subtitle={t("branches.overview.title")} />
<ErrorNotification error={error} />
{activeBranches.length > 0 ? (
@@ -76,7 +77,7 @@ const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
/>
) : null}
{showCreateButton ? <CreateButton label={t("branches.overview.createButton")} link="./create" /> : null}
</>
</KeyboardIterator>
);
};

View File

@@ -30,6 +30,7 @@ import { File, Repository } from "@scm-manager/ui-types";
import FileTreeLeaf from "./FileTreeLeaf";
import TruncatedNotification from "./TruncatedNotification";
import { isRootPath } from "../utils/files";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -69,8 +70,8 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
revision,
_links: {},
_embedded: {
children: []
}
children: [],
},
});
}
@@ -82,7 +83,7 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
repository,
directory,
baseUrl,
revision
revision,
};
return (
@@ -105,9 +106,11 @@ const FileTree: FC<Props> = ({ repository, directory, baseUrl, revision, fetchNe
</tr>
</thead>
<tbody>
<KeyboardIterator>
{files.map((file: File) => (
<FileTreeLeaf key={file.name} file={file} baseUrl={baseUrlWithRevision} repository={repository} />
))}
</KeyboardIterator>
</tbody>
</table>
<TruncatedNotification

View File

@@ -22,15 +22,16 @@
* SOFTWARE.
*/
import * as React from "react";
import { FC, ReactElement } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import classNames from "classnames";
import styled from "styled-components";
import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
import { File, Repository } from "@scm-manager/ui-types";
import { devices, DateFromNow, FileSize, Icon, Tooltip } from "@scm-manager/ui-components";
import { DateFromNow, devices, FileSize, Icon, Tooltip } from "@scm-manager/ui-components";
import FileIcon from "./FileIcon";
import FileLink from "./content/FileLink";
import { ReactElement } from "react";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = WithTranslation & {
repository: Repository;
@@ -58,6 +59,15 @@ const ExtensionTd = styled.td`
}
`;
const FileName: FC<{ file: File; baseUrl: string }> = ({ file, baseUrl }) => {
const ref = useKeyboardIteratorTarget();
return (
<FileLink ref={ref} baseUrl={baseUrl} file={file} tabIndex={0}>
{file.name}
</FileLink>
);
};
class FileTreeLeaf extends React.Component<Props> {
createFileIcon = (file: File) => {
return (
@@ -67,14 +77,6 @@ class FileTreeLeaf extends React.Component<Props> {
);
};
createFileName = (file: File) => {
return (
<FileLink baseUrl={this.props.baseUrl} file={file} tabIndex={0}>
{file.name}
</FileLink>
);
};
contentIfPresent = (file: File, attribute: string, content: (file: File) => ReactElement | string | undefined) => {
const { t } = this.props;
if (file.hasOwnProperty(attribute)) {
@@ -97,27 +99,29 @@ class FileTreeLeaf extends React.Component<Props> {
};
render() {
const { repository, file } = this.props;
const { repository, file, baseUrl } = this.props;
const renderFileSize = (file: File) => <FileSize bytes={file?.length ? file.length : 0} />;
const renderCommitDate = (file: File) => <DateFromNow date={file.commitDate} />;
const extProps: extensionPoints.ReposSourcesTreeRowProps = {
repository,
file
file,
};
return (
<>
<tr>
<td>{this.createFileIcon(file)}</td>
<MinWidthTd className="is-word-break">{this.createFileName(file)}</MinWidthTd>
<MinWidthTd className="is-word-break">
<FileName file={file} baseUrl={baseUrl} />
</MinWidthTd>
<NoWrapTd className="is-hidden-mobile">
{file.directory ? "" : this.contentIfPresent(file, "length", renderFileSize)}
</NoWrapTd>
<td className="is-hidden-mobile">{this.contentIfPresent(file, "commitDate", renderCommitDate)}</td>
<MinWidthTd className={classNames("is-word-break", "is-hidden-touch")}>
{this.contentIfPresent(file, "description", file => file.description)}
{this.contentIfPresent(file, "description", (file) => file.description)}
</MinWidthTd>
{binder.hasExtension<extensionPoints.ReposSourcesTreeRowRight>("repos.sources.tree.row.right", extProps) && (

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React, { FC, ReactNode } from "react";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { File } from "@scm-manager/ui-types";
@@ -63,10 +63,7 @@ export const createRelativeLink = (repositoryUrl: string) => {
export const createFolderLink = (base: string, file: File) => {
let link = base;
if (file.path) {
let path = file.path
.split("/")
.map(encodePart)
.join("/");
let path = file.path.split("/").map(encodePart).join("/");
if (path.startsWith("/")) {
path = path.substring(1);
}
@@ -78,7 +75,7 @@ export const createFolderLink = (base: string, file: File) => {
return link;
};
const FileLink: FC<Props> = ({ baseUrl, file, children, tabIndex }) => {
const FileLink = React.forwardRef<HTMLAnchorElement, Props>(({ baseUrl, file, children, tabIndex }, ref) => {
const [t] = useTranslation("repos");
if (file?.subRepository?.repositoryUrl) {
// file link represents a subRepository
@@ -117,10 +114,10 @@ const FileLink: FC<Props> = ({ baseUrl, file, children, tabIndex }) => {
}
// normal file or folder
return (
<Link to={createFolderLink(baseUrl, file)} tabIndex={tabIndex}>
<Link ref={ref} to={createFolderLink(baseUrl, file)} tabIndex={tabIndex}>
{children}
</Link>
);
};
});
export default FileLink;

View File

@@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { Tag, Link } from "@scm-manager/ui-types";
import { Button, DateFromNow } from "@scm-manager/ui-components";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = {
tag: Tag;
@@ -37,6 +38,7 @@ type Props = {
const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
const [t] = useTranslation("repos");
const ref = useKeyboardIteratorTarget();
let deleteButton;
if ((tag?._links?.delete as Link)?.href) {
@@ -49,7 +51,7 @@ const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
return (
<tr>
<td className="is-vertical-align-middle">
<RouterLink to={to} title={tag.name}>
<RouterLink ref={ref} to={to} title={tag.name}>
{tag.name}
<span className={classNames("has-text-secondary", "is-ellipsis-overflow", "ml-2", "is-size-7")}>
{t("tags.overview.created")} <DateFromNow date={tag.date} />

View File

@@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next";
import TagRow from "./TagRow";
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
import { useDeleteTag } from "@scm-manager/ui-api";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
repository: Repository;
@@ -76,14 +77,14 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
{
label: t("tag.delete.confirmAlert.submit"),
isLoading,
onClick: () => deleteTag()
onClick: () => deleteTag(),
},
{
className: "is-info",
label: t("tag.delete.confirmAlert.cancel"),
onClick: () => abortDelete(),
autofocus: true
}
autofocus: true,
},
]}
close={() => abortDelete()}
/>
@@ -96,9 +97,11 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
</tr>
</thead>
<tbody>
{tags.map(tag => (
<KeyboardIterator>
{tags.map((tag) => (
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
))}
</KeyboardIterator>
</tbody>
</table>
</>

View File

@@ -21,28 +21,26 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { User } from "@scm-manager/ui-types";
import { createAttributesForTesting, Icon } from "@scm-manager/ui-components";
import classNames from "classnames";
import { useKeyboardIteratorTarget } from "@scm-manager/ui-shortcuts";
type Props = WithTranslation & {
type Props = {
user: User;
};
class UserRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return (
<Link to={to} {...createAttributesForTesting(label)}>
{label}
const UserRowLink = React.forwardRef<HTMLAnchorElement, { to: string; children: string }>(({ children, to }, ref) => (
<Link ref={ref} to={to} {...createAttributesForTesting(children)}>
{children}
</Link>
);
}
render() {
const { user, t } = this.props;
));
const UserRow: FC<Props> = ({ user }) => {
const ref = useKeyboardIteratorTarget();
const [t] = useTranslation("users");
const to = `/user/${user.name}`;
const iconType = user.active ? (
<Icon title={t("user.active")} name="user" />
@@ -53,15 +51,19 @@ class UserRow extends React.Component<Props> {
return (
<tr className={user.active ? "border-is-green" : "border-is-yellow"}>
<td className="is-word-break">
{iconType} {this.renderLink(to, user.name)}
{iconType}{" "}
<UserRowLink ref={ref} to={to}>
{user.name}
</UserRowLink>
</td>
<td className={classNames("is-hidden-mobile", "is-word-break")}>
<UserRowLink to={to}>{user.displayName}</UserRowLink>
</td>
<td className={classNames("is-hidden-mobile", "is-word-break")}>{this.renderLink(to, user.displayName)}</td>
<td className={classNames("is-hidden-mobile", "is-word-break")}>
{user.mail ? <a href={`mailto:${user.mail}`}>{user.mail}</a> : null}
</td>
</tr>
);
}
}
};
export default withTranslation("users")(UserRow);
export default UserRow;

View File

@@ -25,6 +25,7 @@ import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import { User } from "@scm-manager/ui-types";
import UserRow from "./UserRow";
import { KeyboardIterator } from "@scm-manager/ui-shortcuts";
type Props = {
users: User[];
@@ -34,6 +35,7 @@ const UserTable: FC<Props> = ({ users }) => {
const [t] = useTranslation("users");
return (
<KeyboardIterator>
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
@@ -48,6 +50,7 @@ const UserTable: FC<Props> = ({ users }) => {
})}
</tbody>
</table>
</KeyboardIterator>
);
};