Fix key navigation in extended menu in source view

Replace headlessui menu in source view with
internal implementation.

Committed-by: Rene Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2023-07-04 09:00:42 +02:00
parent e2c823e2c4
commit 84fbe7ea35
10 changed files with 61 additions and 136 deletions

View File

@@ -0,0 +1,2 @@
- type: fixed
description: Broken file action menu keyboard interaction

View File

@@ -433,7 +433,7 @@ export type RepositoryAvatar = RenderableExtensionPointDefinition<
*/
export type PrimaryRepositoryAvatar = RenderableExtensionPointDefinition<
"repos.repository-avatar.primary",
{ repository: Repository, size: number }
{ repository: Repository; size: number }
>;
/**
@@ -644,7 +644,7 @@ type BaseActionBarOverflowMenuProps = {
category: string;
label: string;
icon: string;
props?: unknown;
props?: Record<string | number | symbol, unknown>;
};
export type ActionMenuProps = BaseActionBarOverflowMenuProps & { action: (props: ContentActionExtensionProps) => void };
@@ -669,15 +669,14 @@ export type RepositoryDeleteButton = RenderableExtensionPointDefinition<
export type RepositoryInformationTableBottom = RenderableExtensionPointDefinition<
"repository.information.table.bottom",
{ repository: Repository }
>;
>;
export type UserInformationTableBottom = RenderableExtensionPointDefinition<
"user.information.table.bottom",
{ user: User }
>;
>;
export type GroupInformationTableBottom = RenderableExtensionPointDefinition<
"group.information.table.bottom",
{ group: Group }
>;
>;

View File

@@ -32,6 +32,8 @@ import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from "reac
const MenuContent = styled(RadixMenu.Content)`
border: var(--scm-border);
background-color: var(--scm-secondary-background);
z-index: 400;
position: relative;
`;
const MenuItem = styled(RadixMenu.Item).attrs({

View File

@@ -362,6 +362,7 @@
"file": "Datei"
},
"content": {
"actionMenuTrigger": "Aktionen",
"historyButton": "History",
"sourcesButton": "Sources",
"annotateButton": "Annotate",

View File

@@ -362,6 +362,7 @@
"file": "File"
},
"content": {
"actionMenuTrigger": "Actions",
"historyButton": "History",
"sourcesButton": "Sources",
"annotateButton": "Annotate",

View File

@@ -24,36 +24,22 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { Icon } from "@scm-manager/ui-components";
import { extensionPoints } from "@scm-manager/ui-extensions";
import { MenuItemContainer } from "./ContentActionMenu";
import { Menu } from "@scm-manager/ui-overlays";
const ActionMenuItem: FC<
extensionPoints.ActionMenuProps & {
active: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
extensionProps: extensionPoints.ContentActionExtensionProps;
}
> = ({ action, active, label, icon, props, extensionProps, ...rest }) => {
> = ({ action, label, props, icon, extensionProps }) => {
const [t] = useTranslation("plugins");
return (
<MenuItemContainer
className={classNames("is-clickable", "is-flex", "is-align-items-centered", {
"has-background-info has-text-white": active,
})}
title={t(label)}
{...props}
{...rest}
onClick={(event) => {
rest.onClick(event);
action(extensionProps);
}}
>
<Icon name={icon} color="inherit" className="pr-5" />
<span>{t(label)}</span>
</MenuItemContainer>
<Menu.Button onSelect={() => action(extensionProps)} {...props}>
<Icon name={icon} className="pr-5 has-text-inherit" />
{t(label)}
</Menu.Button>
);
};

View File

@@ -24,36 +24,12 @@
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
import React, { FC, ReactElement, useState } from "react";
import { Icon } from "@scm-manager/ui-components";
import styled from "styled-components";
import { Menu } from "@headlessui/react";
import { Menu } from "@scm-manager/ui-overlays";
import FallbackMenuButton from "./FallbackMenuButton";
import MenuItem from "./MenuItem";
const MenuButton = styled(Menu.Button)`
background: transparent;
border: none;
font-size: 1.5rem;
height: 2.5rem;
width: 50px;
margin-bottom: 0.5rem;
`;
const MenuItems = styled(Menu.Items)`
padding: 0.5rem;
position: absolute;
z-index: 999;
width: max-content;
border: var(--scm-border);
border-radius: 5px;
background-color: var(--scm-secondary-background);
box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0px 0 1px rgba(10, 10, 10, 0.02);
`;
export const MenuItemContainer = styled.div`
border-radius: 5px;
padding: 0.5rem;
`;
import { Icon } from "@scm-manager/ui-buttons";
import { useTranslation } from "react-i18next";
const HR = styled.hr`
margin: 0.25rem;
@@ -65,6 +41,7 @@ type Props = {
};
const ContentActionMenu: FC<Props> = ({ extensionProps }) => {
const [t] = useTranslation("repos");
const [selectedModal, setSelectedModal] = useState<ReactElement | undefined>();
const extensions = binder.getExtensions<extensionPoints.FileViewActionBarOverflowMenu>(
"repos.sources.content.actionbar.menu",
@@ -81,47 +58,6 @@ const ContentActionMenu: FC<Props> = ({ extensionProps }) => {
{}
);
const renderMenu = () => (
<>
<Menu as="div" className="is-relative">
{({ open }) => (
<>
<MenuButton>
<Icon name="ellipsis-v" className="has-text-default" />
</MenuButton>
{open && (
<div className="has-background-secondary-least">
<MenuItems>
{Object.entries(categories).map(([_category, extensionSet], index) => (
<>
{extensionSet.map((extension) => (
<Menu.Item as={React.Fragment} key={extension.label}>
{({ active }) => {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore onClick prop required but gets provided implicit by the Menu.Item from headless ui
<MenuItem
extensionProps={extensionProps}
active={active}
setSelectedModal={setSelectedModal}
{...extension}
/>
);
}}
</Menu.Item>
))}
{Object.keys(categories).length > index + 1 ? <HR /> : null}
</>
))}
</MenuItems>
</div>
)}
</>
)}
</Menu>
</>
);
if (extensions.length <= 0) {
return null;
}
@@ -135,7 +71,30 @@ const ContentActionMenu: FC<Props> = ({ extensionProps }) => {
setSelectedModal={setSelectedModal}
/>
) : (
renderMenu()
<Menu
trigger={
<Menu.Trigger
className="has-background-transparent has-hover-color-blue px-2"
aria-label={t("sources.content.actionMenuTrigger")}
>
<Icon>ellipsis-v</Icon>
</Menu.Trigger>
}
>
{Object.entries(categories).map(([_category, extensionSet], index) => (
<>
{extensionSet.map((extension) => (
<MenuItem
key={extension.label}
extensionProps={extensionProps}
setSelectedModal={setSelectedModal}
{...extension}
/>
))}
{Object.keys(categories).length > index + 1 ? <HR /> : null}
</>
))}
</Menu>
)}
{selectedModal || null}
</>

View File

@@ -24,11 +24,11 @@
import React, { FC } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { Icon } from "@scm-manager/ui-components";
import styled from "styled-components";
import { Link } from "react-router-dom";
import { extensionPoints } from "@scm-manager/ui-extensions";
import { Menu } from "@scm-manager/ui-overlays";
const MenuItemLinkContainer = styled(Link)<{ active: boolean }>`
border-radius: 5px;
@@ -40,24 +40,15 @@ const MenuItemLinkContainer = styled(Link)<{ active: boolean }>`
`;
const LinkMenuItem: FC<
extensionPoints.LinkMenuProps & { active: boolean; extensionProps: extensionPoints.ContentActionExtensionProps }
> = ({ link, active, label, icon, props, extensionProps, ...rest }) => {
extensionPoints.LinkMenuProps & { extensionProps: extensionPoints.ContentActionExtensionProps }
> = ({ link, label, props, icon, extensionProps }) => {
const [t] = useTranslation("plugins");
return (
<MenuItemLinkContainer
className={classNames("is-clickable", "is-flex", "is-align-items-centered", {
"has-background-info": active,
})}
to={link(extensionProps)}
title={t(label)}
active={active}
{...props}
{...rest}
>
<Icon name={icon} color="inherit" className="pr-5" />
<Menu.Link to={link(extensionProps)} {...props}>
<Icon name={icon} className="pr-5 has-text-inherit" />
<span>{t(label)}</span>
</MenuItemLinkContainer>
</Menu.Link>
);
};

View File

@@ -30,12 +30,10 @@ import { extensionPoints } from "@scm-manager/ui-extensions";
const MenuItem: FC<
extensionPoints.FileViewActionBarOverflowMenu["type"] & {
active: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
setSelectedModal: (element: ReactElement | undefined) => void;
extensionProps: extensionPoints.ContentActionExtensionProps;
}
> = ({ extensionProps, label, icon, props, category, active, onClick, setSelectedModal, ...rest }) => {
> = ({ extensionProps, label, icon, props, category, setSelectedModal, ...rest }) => {
if ("action" in rest) {
return (
<ActionMenuItem
@@ -43,8 +41,6 @@ const MenuItem: FC<
icon={icon}
category={category}
extensionProps={extensionProps}
active={active}
onClick={onClick}
props={props}
{...rest}
/>
@@ -56,7 +52,6 @@ const MenuItem: FC<
category={category}
label={label}
icon={icon}
active={active}
extensionProps={extensionProps}
props={props}
{...rest}
@@ -70,8 +65,6 @@ const MenuItem: FC<
label={label}
icon={icon}
extensionProps={extensionProps}
active={active}
onClick={onClick}
setSelectedModal={setSelectedModal}
props={props}
{...rest}

View File

@@ -24,39 +24,30 @@
import React, { FC, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import { Icon } from "@scm-manager/ui-components";
import { MenuItemContainer } from "./ContentActionMenu";
import { extensionPoints } from "@scm-manager/ui-extensions";
import { Menu } from "@scm-manager/ui-overlays";
const ModalMenuItem: FC<
extensionPoints.ModalMenuProps & {
active: boolean;
onClick: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
setSelectedModal: (element: ReactElement | undefined) => void;
extensionProps: extensionPoints.ContentActionExtensionProps;
}
> = ({ modalElement, active, label, icon, props, extensionProps, setSelectedModal, ...rest }) => {
> = ({ modalElement, label, icon, props, extensionProps, setSelectedModal }) => {
const [t] = useTranslation("plugins");
return (
<MenuItemContainer
className={classNames("is-clickable", "is-flex", "is-align-items-centered", {
"has-background-info has-text-white": active,
})}
title={t(label)}
{...props}
{...rest}
onClick={(event) => {
<Menu.Button
onSelect={() =>
setSelectedModal(
React.createElement(modalElement, { ...extensionProps, close: () => setSelectedModal(undefined) })
);
rest.onClick(event);
}}
)
}
{...props}
>
<Icon name={icon} color="inherit" className="pr-5" />
<Icon name={icon} className="pr-5 has-text-inherit" />
<span>{t(label)}</span>
</MenuItemContainer>
</Menu.Button>
);
};