diff --git a/gradle/changelog/list_navigation.yaml b/gradle/changelog/list_navigation.yaml new file mode 100644 index 0000000000..01637433c0 --- /dev/null +++ b/gradle/changelog/list_navigation.yaml @@ -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)) diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 84f34fbd29..7f07a1743f 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -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", @@ -107,4 +108,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/scm-ui/ui-components/src/CardColumn.tsx b/scm-ui/ui-components/src/CardColumn.tsx index 9d758795f8..fde95d4be4 100644 --- a/scm-ui/ui-components/src/CardColumn.tsx +++ b/scm-ui/ui-components/src/CardColumn.tsx @@ -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,69 +55,72 @@ const InvisibleButton = styled.button` cursor: pointer; `; -const CardColumn: FC = ({ - link, - avatar, - title, - description, - contentRight, - footerLeft, - footerRight, - action, - className, -}) => { - const renderAvatar = avatar ?
{avatar}
: null; - const renderDescription = description ?

{description}

: null; - const renderContentRight = contentRight ?
{contentRight}
: null; +const CardColumn = React.forwardRef( + ({ link, avatar, title, description, contentRight, footerLeft, footerRight, action, className }, ref) => { + const renderAvatar = avatar ?
{avatar}
: null; + const renderDescription = description ?

{description}

: null; + const renderContentRight = contentRight ?
{contentRight}
: 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 = ; - } else if (action) { - createLink = ( - { - e.preventDefault(); - action(); - }} - tabIndex={0} - /> + let createLink = null; + if (link) { + createLink = ; + } else if (action) { + createLink = ( + { + e.preventDefault(); + action(); + }} + tabIndex={0} + /> + ); + } + + return ( + <> + {createLink} + + {renderAvatar} +
+
+
+

{title}

+ {renderDescription} +
+ {renderContentRight} +
+
+
{footerLeft}
+ + {footerRight} + +
+
+
+ ); } - - return ( - <> - {createLink} - - {renderAvatar} -
-
-
-

{title}

- {renderDescription} -
- {renderContentRight} -
-
-
{footerLeft}
- - {footerRight} - -
-
-
- - ); -}; +); export default CardColumn; diff --git a/scm-ui/ui-components/src/CardColumnGroup.tsx b/scm-ui/ui-components/src/CardColumnGroup.tsx index 99f0fbcbec..b71e22d6d9 100644 --- a/scm-ui/ui-components/src/CardColumnGroup.tsx +++ b/scm-ui/ui-components/src/CardColumnGroup.tsx @@ -41,13 +41,13 @@ class CardColumnGroup extends React.Component { 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 { const fullColumnWidth = this.isFullSize(elements, index); const sizeClass = fullColumnWidth ? "is-full" : "is-half"; return ( -
+
{entry}
); diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index 298169fdf1..c05a43d3d9 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -934,6 +934,24 @@ exports[`Storyshots Buttons/Button Loading 1`] = `
`; +exports[`Storyshots Buttons/Button Ref Default 1`] = ` +
+ +
+`; + exports[`Storyshots Buttons/CreateButton Default 1`] = ` + + + + +`; + +exports[`Storyshots Repositories/Changesets List with navigation 1`] = ` +
+
+
+
+
+
+
+
+

+ + 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. + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + + , + changeset.contributors.committedBy + + + Zaphod Beeblebrox + + + commaSeparatedList.lastDivider + + changeset.contributors.coAuthoredBy + + + Ford Prefect + + +

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + Change heading to "Heart Of Gold" + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + + + commaSeparatedList.lastDivider + + changeset.contributors.committedBy + + + Zaphod Beeblebrox + + +

+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+

+ + initialize repository + +

+

+

+

+

+ changeset.contributors.authoredBy + + + SCM Administrator + +

+
+
+
+
+
+
+
+
+
+

+ + + + + changeset.buttons.details + + + +

+

+ @@ -20493,7 +20875,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = ` > @@ -20513,7 +20895,7 @@ exports[`Storyshots Repositories/Changesets Replacements 1`] = ` > @@ -20620,7 +21002,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = ` > @@ -20640,7 +21022,7 @@ exports[`Storyshots Repositories/Changesets With Committer 1`] = ` > @@ -20756,7 +21138,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = ` > @@ -20776,7 +21158,7 @@ exports[`Storyshots Repositories/Changesets With Committer and Co-Author 1`] = ` > @@ -20884,7 +21266,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = ` > @@ -20904,7 +21286,7 @@ exports[`Storyshots Repositories/Changesets With avatar 1`] = ` > @@ -21008,7 +21390,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = ` > @@ -21028,7 +21410,7 @@ exports[`Storyshots Repositories/Changesets With contactless signature 1`] = ` > @@ -21132,7 +21514,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = ` > @@ -21152,7 +21534,7 @@ exports[`Storyshots Repositories/Changesets With invalid signature 1`] = ` > @@ -21260,7 +21642,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = ` > @@ -21280,7 +21662,7 @@ exports[`Storyshots Repositories/Changesets With multiple Co-Authors 1`] = ` > @@ -21384,7 +21766,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid > @@ -21404,7 +21786,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and invalid > @@ -21508,7 +21890,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou > @@ -21528,7 +21910,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and not fou > @@ -21632,7 +22014,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s > @@ -21652,7 +22034,7 @@ exports[`Storyshots Repositories/Changesets With multiple signatures and valid s > @@ -21756,7 +22138,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = ` > @@ -21776,7 +22158,7 @@ exports[`Storyshots Repositories/Changesets With unknown signature 1`] = ` > @@ -21880,7 +22262,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = ` > @@ -21900,7 +22282,7 @@ exports[`Storyshots Repositories/Changesets With unowned signature 1`] = ` > @@ -22004,7 +22386,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = ` > @@ -22024,7 +22406,7 @@ exports[`Storyshots Repositories/Changesets With valid signature 1`] = ` > diff --git a/scm-ui/ui-components/src/buttons/Button.tsx b/scm-ui/ui-components/src/buttons/Button.tsx index 96808aade0..346b09524c 100644 --- a/scm-ui/ui-components/src/buttons/Button.tsx +++ b/scm-ui/ui-components/src/buttons/Button.tsx @@ -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; }; type Props = ButtonProps & { @@ -48,85 +47,95 @@ type Props = ButtonProps & { color?: string; }; -type InnerProps = Props & { - innerRef: React.Ref; -}; - -const Button: FC = ({ - link, - className, - icon, - fullWidth, - reducedMobile, - testId, - children, - label, - type = "button", - title, - loading, - disabled, - action, - color = "default", - innerRef -}) => { - const renderIcon = () => { - return ( - <> - {icon ? ( - - ) : null} - +const Button = React.forwardRef( + ( + { + link, + className, + icon, + fullWidth, + reducedMobile, + testId, + children, + label, + type = "button", + title, + loading, + disabled, + action, + color = "default", + }, + ref + ) => { + const executeRef = useCallback( + (el: HTMLButtonElement | HTMLAnchorElement | null) => { + if (typeof ref === "function") { + ref(el); + } else if (ref) { + ref.current = el; + } + }, + [ref] ); - }; - - const classes = classNames( - "button", - "is-" + color, - { "is-loading": loading }, - { "is-fullwidth": fullWidth }, - { "is-reduced-mobile": reducedMobile }, - className - ); - - const content = ( - - {renderIcon()}{" "} - {(label || children) && ( - <> - {label} {children} - - )} - - ); - - if (link && !disabled) { - if (link.includes("://")) { + const renderIcon = () => { return ( - + <> + {icon ? ( + + ) : null} + + ); + }; + + const classes = classNames( + "button", + "is-" + color, + { "is-loading": loading }, + { "is-fullwidth": fullWidth }, + { "is-reduced-mobile": reducedMobile }, + className + ); + + const content = ( + + {renderIcon()}{" "} + {(label || children) && ( + <> + {label} {children} + + )} + + ); + + if (link && !disabled) { + if (link.includes("://")) { + return ( + + {content} + + ); + } + return ( + {content} - + ); } + return ( - + ); } +); - return ( - - ); -}; - -export default React.forwardRef((props, ref) => ; +}); diff --git a/scm-ui/ui-components/src/modals/useRegisterModal.ts b/scm-ui/ui-components/src/modals/useRegisterModal.ts index 2d73951b1c..d01f2548b5 100644 --- a/scm-ui/ui-components/src/modals/useRegisterModal.ts +++ b/scm-ui/ui-components/src/modals/useRegisterModal.ts @@ -37,16 +37,22 @@ export default function useRegisterModal(active: boolean, initialValue: boolean useEffect(() => { if (active) { previousActiveState.current = true; - increment(); + if (increment) { + increment(); + } } else { if (previousActiveState.current !== null) { - decrement(); + if (decrement) { + decrement(); + } } previousActiveState.current = false; } return () => { if (previousActiveState.current) { - decrement(); + if (decrement) { + decrement(); + } previousActiveState.current = null; } }; diff --git a/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx b/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx index 7c85c31244..29d873cf40 100644 --- a/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx +++ b/scm-ui/ui-components/src/repos/changesets/ChangesetButtonGroup.tsx @@ -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 = ({ repository, changeset, file }) => { - const [t] = useTranslation("repos"); - const changesetLink = createChangesetLink(repository, changeset); - const sourcesLink = createSourcesLink(repository, changeset, file); - return ( - - - - - ); -}; +const ChangesetButtonGroup = React.forwardRef( + ({ repository, changeset, file }, ref) => { + const [t] = useTranslation("repos"); + const changesetLink = createChangesetLink(repository, changeset); + const sourcesLink = createSourcesLink(repository, changeset, file); + return ( + +