mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-05 03:40:56 +01:00
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:
2
gradle/changelog/file_action_menu_accessibility.yaml
Normal file
2
gradle/changelog/file_action_menu_accessibility.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: fixed
|
||||
description: Broken file action menu keyboard interaction
|
||||
@@ -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 }
|
||||
>;
|
||||
|
||||
>;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"file": "Datei"
|
||||
},
|
||||
"content": {
|
||||
"actionMenuTrigger": "Aktionen",
|
||||
"historyButton": "History",
|
||||
"sourcesButton": "Sources",
|
||||
"annotateButton": "Annotate",
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
"file": "File"
|
||||
},
|
||||
"content": {
|
||||
"actionMenuTrigger": "Actions",
|
||||
"historyButton": "History",
|
||||
"sourcesButton": "Sources",
|
||||
"annotateButton": "Annotate",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user