@scm-manager/ui-components 3.10.4-20250824-132529 → 4.0.0-REACT18-20250824-143504
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +45 -50
- package/src/BranchSelector.stories.tsx +60 -11
- package/src/Breadcrumb.stories.tsx +131 -57
- package/src/Breadcrumb.tsx +3 -3
- package/src/CardColumn.stories.tsx +94 -27
- package/src/CardColumnSmall.stories.tsx +102 -27
- package/src/CommaSeparatedList.tsx +2 -2
- package/src/Date.stories.tsx +64 -17
- package/src/Duration.stories.tsx +92 -45
- package/src/ErrorBoundary.tsx +38 -58
- package/src/ErrorNotification.tsx +1 -1
- package/src/Help.stories.tsx +61 -17
- package/src/Help.tsx +5 -11
- package/src/Icon.stories.tsx +93 -13
- package/src/Image.tsx +2 -3
- package/src/LinkPaginator.tsx +9 -6
- package/src/Loading.stories.tsx +26 -6
- package/src/Logo.stories.tsx +44 -13
- package/src/Notification.stories.tsx +111 -23
- package/src/OverviewPageActions.tsx +5 -5
- package/src/Paginator.test.tsx +115 -94
- package/src/PdfViewer.stories.tsx +83 -5
- package/src/PreformattedCodeBlock.stories.tsx +97 -26
- package/src/ProtectedRoute.tsx +14 -39
- package/src/SmallLoadingSpinner.stories.tsx +26 -6
- package/src/SyntaxHighlighter.stories.tsx +122 -40
- package/src/SyntaxHighlighterRenderer.tsx +7 -7
- package/src/Tag.stories.tsx +135 -18
- package/src/Tag.tsx +2 -1
- package/src/Tooltip.stories.tsx +147 -34
- package/src/__snapshots__/storyshots.test.ts.snap +21 -144
- package/src/avatar/Avatar.ts +1 -1
- package/src/avatar/AvatarImage.tsx +2 -2
- package/src/avatar/AvatarWrapper.tsx +2 -2
- package/src/buttons/Button.stories.tsx +159 -0
- package/src/buttons/Button.tsx +5 -5
- package/src/config/ConfigurationBinder.tsx +39 -39
- package/src/config/ConfigurationForm.tsx +2 -1
- package/src/config/TitledSettings.tsx +4 -1
- package/src/forms/AddKeyValueEntryToTableField.stories.tsx +60 -25
- package/src/forms/Checkbox.stories.tsx +165 -27
- package/src/forms/Checkbox.tsx +1 -0
- package/src/forms/DropDown.stories.tsx +81 -35
- package/src/forms/FileInput.stories.tsx +85 -10
- package/src/forms/InputField.stories.tsx +210 -37
- package/src/forms/InputField.tsx +1 -0
- package/src/forms/Radio.stories.tsx +181 -34
- package/src/forms/Radio.tsx +1 -0
- package/src/forms/Select.stories.tsx +266 -46
- package/src/forms/Select.tsx +1 -0
- package/src/forms/Textarea.stories.tsx +99 -53
- package/src/forms/Textarea.tsx +1 -0
- package/src/forms/index.ts +3 -1
- package/src/index.ts +5 -5
- package/src/jest-dom.d.ts +17 -0
- package/src/layout/Footer.stories.tsx +114 -22
- package/src/layout/Footer.tsx +11 -7
- package/src/layout/FooterSection.tsx +1 -0
- package/src/layout/Header.tsx +2 -1
- package/src/layout/Page.tsx +1 -3
- package/src/layout/PrimaryContentColumn.tsx +2 -2
- package/src/layout/SecondaryNavigationColumn.tsx +3 -3
- package/src/layout/SubSubtitle.tsx +2 -1
- package/src/layout/Subtitle.tsx +2 -1
- package/src/layout/Title.tsx +2 -5
- package/src/markdown/LazyMarkdownView.tsx +16 -5
- package/src/markdown/MarkdownHeadingRenderer.test.ts +9 -1
- package/src/markdown/MarkdownHeadingRenderer.tsx +3 -3
- package/src/markdown/MarkdownImageRenderer.test.ts +8 -1
- package/src/markdown/MarkdownImageRenderer.tsx +2 -1
- package/src/markdown/MarkdownLinkRenderer.test.tsx +9 -0
- package/src/markdown/MarkdownLinkRenderer.tsx +6 -4
- package/src/markdown/MarkdownView.stories.tsx +224 -72
- package/src/markdown/markdownExtensions.ts +6 -4
- package/src/modals/ActiveModalCountContextProvider.tsx +2 -2
- package/src/modals/ConfirmAlert.stories.tsx +133 -44
- package/src/modals/ConfirmAlert.tsx +5 -4
- package/src/modals/Modal.stories.tsx +461 -252
- package/src/modals/Modal.tsx +12 -11
- package/src/modals/useRegisterModal.test.tsx +5 -4
- package/src/navigation/MenuContext.tsx +2 -2
- package/src/navigation/NavLink.tsx +4 -7
- package/src/navigation/PrimaryNavigation.tsx +2 -2
- package/src/navigation/PrimaryNavigationLink.tsx +6 -5
- package/src/navigation/RoutingProps.ts +1 -3
- package/src/navigation/SecondaryNavigation.stories.tsx +157 -45
- package/src/navigation/SecondaryNavigation.tsx +17 -16
- package/src/navigation/SecondaryNavigationItem.tsx +2 -1
- package/src/navigation/SubNavigation.tsx +4 -8
- package/src/navigation/useActiveMatch.ts +20 -22
- package/src/navigation/useNavigationLock.ts +1 -0
- package/src/popover/Popover.stories.tsx +111 -41
- package/src/popover/Popover.tsx +2 -5
- package/src/repos/Diff.stories.tsx +418 -223
- package/src/repos/Diff.tsx +1 -5
- package/src/repos/DiffTypes.ts +2 -14
- package/src/repos/HunkExpandDivider.tsx +2 -2
- package/src/repos/LoadingDiff.tsx +11 -6
- package/src/repos/RepositoryEntry.stories.tsx +217 -53
- package/src/repos/RepositoryFlag.tsx +2 -1
- package/src/repos/TokenizedDiffView.tsx +4 -2
- package/src/repos/annotate/Annotate.stories.tsx +225 -111
- package/src/repos/annotate/AnnotateLine.tsx +2 -1
- package/src/repos/changesets/ChangesetAuthor.tsx +4 -4
- package/src/repos/changesets/ChangesetButtonGroup.test.tsx +9 -5
- package/src/repos/changesets/ChangesetDiff.test.ts +10 -2
- package/src/repos/changesets/Changesets.stories.tsx +388 -197
- package/src/repos/changesets/Contributor.tsx +1 -1
- package/src/repos/changesets/ContributorRow.tsx +2 -2
- package/src/repos/changesets/SignatureIcon.tsx +1 -0
- package/src/repos/changesets/SingleChangeset.tsx +0 -1
- package/src/repos/diff/DiffFileTree.tsx +37 -89
- package/src/repos/diff/styledElements.tsx +4 -3
- package/src/repos/index.ts +15 -15
- package/src/search/Hit.tsx +3 -2
- package/src/search/TextHitField.stories.tsx +131 -43
- package/src/search/TextHitField.tsx +3 -2
- package/src/search/index.ts +1 -1
- package/src/storyshots.test.ts +66 -60
- package/src/table/Table.stories.tsx +146 -48
- package/src/table/Table.tsx +7 -8
- package/src/table/TextColumn.tsx +0 -9
- package/src/toast/Toast.tsx +2 -1
- package/src/toast/ToastArea.tsx +2 -2
- package/src/toast/ToastButton.tsx +2 -1
- package/src/toast/ToastButtons.tsx +4 -2
- package/src/toast/ToastNotification.tsx +2 -1
- package/src/toast/index.stories.tsx +144 -39
- package/src/toast/index.ts +1 -1
- package/src/buttons/index.stories.tsx +0 -85
package/src/modals/Modal.tsx
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC, MutableRefObject, useRef } from "react";
|
|
17
|
+
import React, { FC, MutableRefObject, ReactNode, useRef } from "react";
|
|
18
18
|
import classNames from "classnames";
|
|
19
19
|
import styled from "styled-components";
|
|
20
|
-
import { Dialog } from "@headlessui/react";
|
|
20
|
+
import { Dialog, DialogTitle } from "@headlessui/react";
|
|
21
21
|
import { devices } from "../devices";
|
|
22
22
|
import useRegisterModal from "./useRegisterModal";
|
|
23
23
|
|
|
@@ -37,13 +37,14 @@ type Props = {
|
|
|
37
37
|
size?: ModalSize;
|
|
38
38
|
initialFocusRef?: MutableRefObject<HTMLElement | null>;
|
|
39
39
|
overflowVisible?: boolean;
|
|
40
|
+
children?: ReactNode;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
const SizedModal = styled.div<{ size?: ModalSize; overflow: string }>`
|
|
43
|
-
width: ${props => (props.size ? `${modalSizes[props.size]}%` : "640px")};
|
|
44
|
-
overflow: ${props => props.overflow};
|
|
44
|
+
width: ${(props) => (props.size ? `${modalSizes[props.size]}%` : "640px")};
|
|
45
|
+
overflow: ${(props) => props.overflow};
|
|
45
46
|
@media screen and (max-width: ${devices.mobile.width}px) {
|
|
46
|
-
width: ${props => (props.size ? `${modalSizes[props.size]}%` : "320px")};
|
|
47
|
+
width: ${(props) => (props.size ? `${modalSizes[props.size]}%` : "320px")};
|
|
47
48
|
}
|
|
48
49
|
`;
|
|
49
50
|
|
|
@@ -59,7 +60,7 @@ export const Modal: FC<Props> = ({
|
|
|
59
60
|
headTextColor = "secondary-most",
|
|
60
61
|
size,
|
|
61
62
|
initialFocusRef,
|
|
62
|
-
overflowVisible
|
|
63
|
+
overflowVisible,
|
|
63
64
|
}) => {
|
|
64
65
|
useRegisterModal(active);
|
|
65
66
|
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
|
|
@@ -81,13 +82,13 @@ export const Modal: FC<Props> = ({
|
|
|
81
82
|
{ "is-active": active },
|
|
82
83
|
`is-overflow-${overflowAttribute}`,
|
|
83
84
|
`is-border-bottom-radius-${borderBottomRadiusAttribute}`,
|
|
84
|
-
|
|
85
|
+
"modal-background",
|
|
86
|
+
className,
|
|
85
87
|
)}
|
|
86
88
|
initialFocus={initialFocusRef ?? closeButtonRef}
|
|
87
89
|
>
|
|
88
|
-
<Dialog.Overlay className="modal-background" />
|
|
89
90
|
<SizedModal className="modal-card" size={size} overflow={overflowAttribute}>
|
|
90
|
-
<
|
|
91
|
+
<DialogTitle as="header" className={classNames("modal-card-head", `has-background-${headColor}`)}>
|
|
91
92
|
<h2 className={`modal-card-title m-0 has-text-${headTextColor}`}>{title}</h2>
|
|
92
93
|
<button
|
|
93
94
|
className="delete"
|
|
@@ -95,12 +96,12 @@ export const Modal: FC<Props> = ({
|
|
|
95
96
|
onClick={closeFunction}
|
|
96
97
|
ref={!initialFocusRef ? closeButtonRef : undefined}
|
|
97
98
|
/>
|
|
98
|
-
</
|
|
99
|
+
</DialogTitle>
|
|
99
100
|
<section
|
|
100
101
|
className={classNames(
|
|
101
102
|
"modal-card-body",
|
|
102
103
|
`is-overflow-${overflowAttribute}`,
|
|
103
|
-
`is-border-bottom-radius-${borderBottomRadiusAttribute}
|
|
104
|
+
`is-border-bottom-radius-${borderBottomRadiusAttribute}`,
|
|
104
105
|
)}
|
|
105
106
|
>
|
|
106
107
|
{body || children}
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { renderHook } from "@testing-library/react
|
|
18
|
-
import React, { FC } from "react";
|
|
17
|
+
import { renderHook } from "@testing-library/react";
|
|
18
|
+
import React, { FC, ReactNode } from "react";
|
|
19
19
|
import ActiveModalCountContext, { ModalStateContextType } from "./activeModalCountContext";
|
|
20
20
|
import useRegisterModal from "./useRegisterModal";
|
|
21
21
|
|
|
22
22
|
const createWrapper =
|
|
23
|
-
(context: ModalStateContextType): FC =>
|
|
24
|
-
({ children }) => {
|
|
23
|
+
(context: ModalStateContextType): FC<{ children: ReactNode }> =>
|
|
24
|
+
({ children }: { children: React.ReactNode }) => {
|
|
25
25
|
return <ActiveModalCountContext.Provider value={context}>{children}</ActiveModalCountContext.Provider>;
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -83,6 +83,7 @@ describe("useRegisterModal", () => {
|
|
|
83
83
|
const decrement = jest.fn();
|
|
84
84
|
|
|
85
85
|
const result = renderHook(({ active }: { active: boolean }) => useRegisterModal(active), {
|
|
86
|
+
// @ts-ignore
|
|
86
87
|
wrapper: createWrapper({ value: 0, increment, decrement }),
|
|
87
88
|
initialProps: { active: true },
|
|
88
89
|
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC, useContext, useState } from "react";
|
|
17
|
+
import React, { FC, ReactNode, useContext, useState } from "react";
|
|
18
18
|
|
|
19
19
|
export type MenuContext = {
|
|
20
20
|
isCollapsed: () => boolean;
|
|
@@ -31,7 +31,7 @@ export const MenuContext = React.createContext<MenuContext>({
|
|
|
31
31
|
setCollapsed() {},
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
export const StateMenuContextProvider: FC = ({ children }) => {
|
|
34
|
+
export const StateMenuContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
|
35
35
|
const [collapsed, setCollapsed] = useState(false);
|
|
36
36
|
|
|
37
37
|
const context = {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC, useContext, useEffect } from "react";
|
|
17
|
+
import React, { FC, ReactNode, useContext, useEffect } from "react";
|
|
18
18
|
import classNames from "classnames";
|
|
19
19
|
import { Link } from "react-router-dom";
|
|
20
20
|
import { createAttributesForTesting } from "@scm-manager/ui-core";
|
|
@@ -28,6 +28,7 @@ type Props = RoutingProps & {
|
|
|
28
28
|
title?: string;
|
|
29
29
|
icon?: string;
|
|
30
30
|
testId?: string;
|
|
31
|
+
children?: ReactNode;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
type NavLinkContentProp = {
|
|
@@ -47,8 +48,8 @@ const NavLinkContent: FC<NavLinkContentProp> = ({ label, icon, collapsed }) => (
|
|
|
47
48
|
</>
|
|
48
49
|
);
|
|
49
50
|
|
|
50
|
-
const NavLink: FC<Props> = ({ to,
|
|
51
|
-
const active = useActiveMatch({ to,
|
|
51
|
+
const NavLink: FC<Props> = ({ to, activeOnlyWhenExact, activeWhenMatch, title, testId, children, ...contentProps }) => {
|
|
52
|
+
const active = useActiveMatch({ to, activeOnlyWhenExact, activeWhenMatch });
|
|
52
53
|
const { collapsed, setCollapsible } = useSecondaryNavigation();
|
|
53
54
|
const isSubNavigation = useContext(SubNavigationContext);
|
|
54
55
|
|
|
@@ -73,8 +74,4 @@ const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, title, t
|
|
|
73
74
|
);
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
NavLink.defaultProps = {
|
|
77
|
-
activeOnlyWhenExact: true,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
77
|
export default NavLink;
|
|
@@ -53,7 +53,7 @@ const PrimaryNavigation: FC<Props> = ({ links }) => {
|
|
|
53
53
|
|
|
54
54
|
const extensionProps = {
|
|
55
55
|
links,
|
|
56
|
-
label: t("primary-navigation.first-menu")
|
|
56
|
+
label: t("primary-navigation.first-menu"),
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const append = createNavigationAppender(navItems);
|
|
@@ -79,7 +79,7 @@ const PrimaryNavigation: FC<Props> = ({ links }) => {
|
|
|
79
79
|
name="primary-navigation"
|
|
80
80
|
renderAll={true}
|
|
81
81
|
props={{
|
|
82
|
-
links
|
|
82
|
+
links,
|
|
83
83
|
}}
|
|
84
84
|
/>
|
|
85
85
|
);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import React, { FC } from "react";
|
|
18
|
-
import {
|
|
18
|
+
import { useLocation, NavLink } from "react-router-dom";
|
|
19
19
|
import { createAttributesForTesting } from "../devBuild";
|
|
20
20
|
import classNames from "classnames";
|
|
21
21
|
|
|
@@ -28,16 +28,17 @@ type Props = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
const PrimaryNavigationLink: FC<Props> = ({ to, match, testId, label, className }) => {
|
|
31
|
-
const
|
|
31
|
+
const location = useLocation();
|
|
32
|
+
const isActive = new RegExp(match).test(location.pathname);
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
|
-
<
|
|
35
|
+
<NavLink
|
|
35
36
|
to={to}
|
|
36
|
-
className={classNames(className, "navbar-item", { "is-active":
|
|
37
|
+
className={() => classNames(className, "navbar-item", { "is-active": isActive })}
|
|
37
38
|
{...createAttributesForTesting(testId)}
|
|
38
39
|
>
|
|
39
40
|
{label}
|
|
40
|
-
</
|
|
41
|
+
</NavLink>
|
|
41
42
|
);
|
|
42
43
|
};
|
|
43
44
|
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { RouteProps } from "react-router-dom";
|
|
18
|
-
|
|
19
17
|
export type RoutingProps = {
|
|
20
18
|
to: string;
|
|
21
19
|
activeOnlyWhenExact?: boolean;
|
|
22
|
-
activeWhenMatch?:
|
|
20
|
+
activeWhenMatch?: string;
|
|
23
21
|
};
|
|
@@ -14,16 +14,101 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { storiesOf } from "@storybook/react";
|
|
18
|
-
import React, { ReactElement } from "react";
|
|
17
|
+
// import { storiesOf } from "@storybook/react";
|
|
18
|
+
// import React, { ReactElement } from "react";
|
|
19
|
+
// import { MemoryRouter } from "react-router-dom";
|
|
20
|
+
// import styled from "styled-components";
|
|
21
|
+
// import { Binder, ExtensionPoint, BinderContext } from "@scm-manager/ui-extensions";
|
|
22
|
+
// import { SecondaryNavigationProvider } from "./SecondaryNavigationContext";
|
|
23
|
+
// import SecondaryNavigation from "./SecondaryNavigation";
|
|
24
|
+
// import SecondaryNavigationItem from "./SecondaryNavigationItem";
|
|
25
|
+
// import SubNavigation from "./SubNavigation";
|
|
26
|
+
//
|
|
27
|
+
// const Columns = styled.div`
|
|
28
|
+
// margin: 2rem;
|
|
29
|
+
// `;
|
|
30
|
+
//
|
|
31
|
+
// const starships = (
|
|
32
|
+
// <SubNavigation to="/hitchhiker/starships" label="Starships">
|
|
33
|
+
// <SecondaryNavigationItem
|
|
34
|
+
// to="/hitchhiker/starships/heart-of-gold"
|
|
35
|
+
// icon="fas fa-star"
|
|
36
|
+
// label="Heart Of Gold"
|
|
37
|
+
// title="Heart Of Gold"
|
|
38
|
+
// />
|
|
39
|
+
// <SecondaryNavigationItem to="/hitchhiker/starships/titanic" label="Titanic" title="Starship Titanic" />
|
|
40
|
+
// </SubNavigation>
|
|
41
|
+
// );
|
|
42
|
+
//
|
|
43
|
+
// const withRoute = (route: string) => {
|
|
44
|
+
// return (story: ReactElement<any>) => <MemoryRouter initialEntries={[route]}>{story}</MemoryRouter>;
|
|
45
|
+
// };
|
|
46
|
+
//
|
|
47
|
+
// storiesOf("Secondary Navigation", module)
|
|
48
|
+
// .addDecorator((story) => (
|
|
49
|
+
// <Columns className="columns">
|
|
50
|
+
// <div className="column is-3">
|
|
51
|
+
// <SecondaryNavigationProvider>{story()}</SecondaryNavigationProvider>
|
|
52
|
+
// </div>
|
|
53
|
+
// </Columns>
|
|
54
|
+
// ))
|
|
55
|
+
// .add("Default", () =>
|
|
56
|
+
// withRoute("/")(
|
|
57
|
+
// <SecondaryNavigation label="Hitchhiker">
|
|
58
|
+
// <SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
59
|
+
// <SecondaryNavigationItem to="/heart-of-gold" icon="fas fa-star" label="Heart Of Gold" title="Heart Of Gold" />
|
|
60
|
+
// </SecondaryNavigation>
|
|
61
|
+
// )
|
|
62
|
+
// )
|
|
63
|
+
// .add("Sub Navigation", () =>
|
|
64
|
+
// withRoute("/")(
|
|
65
|
+
// <SecondaryNavigation label="Hitchhiker">
|
|
66
|
+
// <SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
67
|
+
// {starships}
|
|
68
|
+
// </SecondaryNavigation>
|
|
69
|
+
// )
|
|
70
|
+
// )
|
|
71
|
+
// .add("Extension Point", () => {
|
|
72
|
+
// const binder = new Binder("menu");
|
|
73
|
+
// binder.bind("subnav.sample", starships);
|
|
74
|
+
// return withRoute("/hitchhiker/starships/titanic")(
|
|
75
|
+
// <BinderContext.Provider value={binder}>
|
|
76
|
+
// <SecondaryNavigation label="Hitchhiker">
|
|
77
|
+
// <SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
78
|
+
// <ExtensionPoint name="subnav.sample" renderAll={true} />
|
|
79
|
+
// </SecondaryNavigation>
|
|
80
|
+
// </BinderContext.Provider>
|
|
81
|
+
// );
|
|
82
|
+
// })
|
|
83
|
+
// .add("Active when match", () =>
|
|
84
|
+
// withRoute("/hog")(
|
|
85
|
+
// <SecondaryNavigation label="Hitchhiker">
|
|
86
|
+
// <SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
87
|
+
// {/*TODO: fix after useAcitveMatch works*/}
|
|
88
|
+
// <SecondaryNavigationItem
|
|
89
|
+
// // activeWhenMatch={(route) => route.location?.pathname === "/hog"}
|
|
90
|
+
// to="/heart-of-gold"
|
|
91
|
+
// icon="fas fa-star"
|
|
92
|
+
// label="Heart Of Gold"
|
|
93
|
+
// title="Heart Of Gold"
|
|
94
|
+
// />
|
|
95
|
+
// </SecondaryNavigation>
|
|
96
|
+
// )
|
|
97
|
+
// );
|
|
98
|
+
|
|
99
|
+
import React from "react";
|
|
19
100
|
import { MemoryRouter } from "react-router-dom";
|
|
20
101
|
import styled from "styled-components";
|
|
102
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
21
103
|
import { Binder, ExtensionPoint, BinderContext } from "@scm-manager/ui-extensions";
|
|
104
|
+
|
|
22
105
|
import { SecondaryNavigationProvider } from "./SecondaryNavigationContext";
|
|
23
106
|
import SecondaryNavigation from "./SecondaryNavigation";
|
|
24
107
|
import SecondaryNavigationItem from "./SecondaryNavigationItem";
|
|
25
108
|
import SubNavigation from "./SubNavigation";
|
|
26
109
|
|
|
110
|
+
// --- Helfer-Komponenten und Mock-Daten (aus der Original-Story übernommen) ---
|
|
111
|
+
|
|
27
112
|
const Columns = styled.div`
|
|
28
113
|
margin: 2rem;
|
|
29
114
|
`;
|
|
@@ -40,38 +125,61 @@ const starships = (
|
|
|
40
125
|
</SubNavigation>
|
|
41
126
|
);
|
|
42
127
|
|
|
43
|
-
|
|
44
|
-
|
|
128
|
+
// --- Storybook Metadaten ---
|
|
129
|
+
|
|
130
|
+
const meta: Meta<typeof SecondaryNavigation> = {
|
|
131
|
+
title: "Navigation/SecondaryNavigation",
|
|
132
|
+
component: SecondaryNavigation,
|
|
133
|
+
// Dieser Decorator wendet das Layout und den notwendigen Provider auf alle Stories an.
|
|
134
|
+
decorators: [
|
|
135
|
+
(Story) => <MemoryRouter initialEntries={["/"]}>{Story()}</MemoryRouter>,
|
|
136
|
+
(Story) => (
|
|
137
|
+
<Columns className="columns">
|
|
138
|
+
<div className="column is-3">
|
|
139
|
+
<SecondaryNavigationProvider>
|
|
140
|
+
<Story />
|
|
141
|
+
</SecondaryNavigationProvider>
|
|
142
|
+
</div>
|
|
143
|
+
</Columns>
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
tags: ["autodocs"],
|
|
45
147
|
};
|
|
46
148
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
149
|
+
export default meta;
|
|
150
|
+
|
|
151
|
+
// --- Story-Definitionen ---
|
|
152
|
+
|
|
153
|
+
type Story = StoryObj<typeof meta>;
|
|
154
|
+
|
|
155
|
+
// Da jede Story eine andere initiale Route für den MemoryRouter benötigt,
|
|
156
|
+
// verwenden wir für jede Story eine `render`-Funktion.
|
|
157
|
+
|
|
158
|
+
export const Default: Story = {
|
|
159
|
+
render: () => (
|
|
160
|
+
<SecondaryNavigation label="Hitchhiker">
|
|
161
|
+
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
162
|
+
<SecondaryNavigationItem to="/heart-of-gold" icon="fas fa-star" label="Heart Of Gold" title="Heart Of Gold" />
|
|
163
|
+
</SecondaryNavigation>
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const WithSubNavigation: Story = {
|
|
168
|
+
name: "With Sub-Navigation",
|
|
169
|
+
render: () => (
|
|
170
|
+
<SecondaryNavigation label="Hitchhiker">
|
|
171
|
+
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
172
|
+
{starships}
|
|
173
|
+
</SecondaryNavigation>
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const WithExtensionPoint: Story = {
|
|
178
|
+
name: "With Extension Point",
|
|
179
|
+
render: () => {
|
|
72
180
|
const binder = new Binder("menu");
|
|
73
181
|
binder.bind("subnav.sample", starships);
|
|
74
|
-
return
|
|
182
|
+
return (
|
|
75
183
|
<BinderContext.Provider value={binder}>
|
|
76
184
|
<SecondaryNavigation label="Hitchhiker">
|
|
77
185
|
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
@@ -79,18 +187,22 @@ storiesOf("Secondary Navigation", module)
|
|
|
79
187
|
</SecondaryNavigation>
|
|
80
188
|
</BinderContext.Provider>
|
|
81
189
|
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const ActiveWhenMatch: Story = {
|
|
194
|
+
name: "Active When Match",
|
|
195
|
+
render: () => (
|
|
196
|
+
<SecondaryNavigation label="Hitchhiker">
|
|
197
|
+
<SecondaryNavigationItem to="/42" icon="fas fa-puzzle-piece" label="Puzzle 42" title="Puzzle 42" />
|
|
198
|
+
{/*TODO: fix after useAcitveMatch works*/}
|
|
199
|
+
<SecondaryNavigationItem
|
|
200
|
+
// activeWhenMatch={(route) => route.location?.pathname === "/hog"}
|
|
201
|
+
to="/heart-of-gold"
|
|
202
|
+
icon="fas fa-star"
|
|
203
|
+
label="Heart Of Gold"
|
|
204
|
+
title="Heart Of Gold"
|
|
205
|
+
/>
|
|
206
|
+
</SecondaryNavigation>
|
|
207
|
+
),
|
|
208
|
+
};
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC } from "react";
|
|
17
|
+
import React, { FC, ReactNode } from "react";
|
|
18
18
|
import styled from "styled-components";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
20
|
import { useSecondaryNavigation } from "../useSecondaryNavigation";
|
|
@@ -23,6 +23,7 @@ import { Button } from "@scm-manager/ui-buttons";
|
|
|
23
23
|
type Props = {
|
|
24
24
|
label: string;
|
|
25
25
|
collapsible?: boolean;
|
|
26
|
+
children?: ReactNode;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
type CollapsedProps = {
|
|
@@ -51,29 +52,29 @@ const Icon = styled.i<CollapsedProps>`
|
|
|
51
52
|
color: #e2e2e2;
|
|
52
53
|
`;
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
-
justify-content: ${(props: CollapsedProps) => (props.collapsed ? "center" : "inherit")};
|
|
56
|
-
width: 100%;
|
|
57
|
-
color: #e2e2e2;
|
|
58
|
-
&:hover {
|
|
59
|
-
color: #e2e2e2;
|
|
60
|
-
}
|
|
61
|
-
&:focus {
|
|
62
|
-
color: #e2e2e2;
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
const SecondaryNavigation: FC<Props> = ({ label, children, collapsible = true }) => {
|
|
55
|
+
const SecondaryNavigation: FC<Props> = ({ label, children }) => {
|
|
67
56
|
const [t] = useTranslation("commons");
|
|
68
|
-
const { collapsible: isCollapsible, collapsed, toggleCollapse } = useSecondaryNavigation(
|
|
57
|
+
const { collapsible: isCollapsible, collapsed, toggleCollapse } = useSecondaryNavigation();
|
|
69
58
|
|
|
70
59
|
const arrowIcon = collapsed ? <i className="fas fa-caret-left" /> : <i className="fas fa-caret-down" />;
|
|
71
60
|
const menuAriaLabel = collapsed ? t("secondaryNavigation.showContent") : t("secondaryNavigation.hideContent");
|
|
72
61
|
|
|
62
|
+
const MenuButton = styled(Button)`
|
|
63
|
+
justify-content: ${collapsed ? "center" : "inherit"};
|
|
64
|
+
width: 100%;
|
|
65
|
+
color: #e2e2e2;
|
|
66
|
+
&:hover {
|
|
67
|
+
color: #e2e2e2;
|
|
68
|
+
}
|
|
69
|
+
&:focus {
|
|
70
|
+
color: #e2e2e2;
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
|
|
73
74
|
return (
|
|
74
75
|
<SectionContainer className="menu" collapsed={collapsed}>
|
|
75
76
|
<div>
|
|
76
|
-
<MenuButton className="menu-label"
|
|
77
|
+
<MenuButton className="menu-label" onClick={toggleCollapse} aria-label={menuAriaLabel}>
|
|
77
78
|
{isCollapsible ? (
|
|
78
79
|
<Icon className="is-medium" collapsed={collapsed}>
|
|
79
80
|
{arrowIcon}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC } from "react";
|
|
17
|
+
import React, { FC, ReactNode } from "react";
|
|
18
18
|
import SubNavigation from "./SubNavigation";
|
|
19
19
|
import NavLink from "./NavLink";
|
|
20
20
|
import { RoutingProps } from "./RoutingProps";
|
|
@@ -23,6 +23,7 @@ type Props = RoutingProps & {
|
|
|
23
23
|
label: string;
|
|
24
24
|
title?: string;
|
|
25
25
|
icon?: string;
|
|
26
|
+
children?: ReactNode;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const SecondaryNavigationItem: FC<Props> = ({ children, ...props }) => {
|
|
@@ -14,20 +14,21 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import React, { FC, useEffect } from "react";
|
|
17
|
+
import React, { FC, ReactNode, useEffect } from "react";
|
|
18
18
|
import { Link } from "react-router-dom";
|
|
19
19
|
import classNames from "classnames";
|
|
20
20
|
import { useSecondaryNavigation } from "../useSecondaryNavigation";
|
|
21
21
|
import { RoutingProps } from "./RoutingProps";
|
|
22
|
-
import useActiveMatch from "./useActiveMatch";
|
|
23
22
|
import { createAttributesForTesting } from "../devBuild";
|
|
24
23
|
import { SubNavigationContext } from "./SubNavigationContext";
|
|
24
|
+
import useActiveMatch from "./useActiveMatch";
|
|
25
25
|
|
|
26
26
|
type Props = RoutingProps & {
|
|
27
27
|
label: string;
|
|
28
28
|
title?: string;
|
|
29
29
|
icon?: string;
|
|
30
30
|
testId?: string;
|
|
31
|
+
children?: ReactNode;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
const SubNavigation: FC<Props> = ({
|
|
@@ -44,12 +45,7 @@ const SubNavigation: FC<Props> = ({
|
|
|
44
45
|
const parents = to.split("/");
|
|
45
46
|
parents.splice(-1, 1);
|
|
46
47
|
const parent = parents.join("/");
|
|
47
|
-
|
|
48
|
-
const active = useActiveMatch({
|
|
49
|
-
to: parent,
|
|
50
|
-
activeOnlyWhenExact,
|
|
51
|
-
activeWhenMatch,
|
|
52
|
-
});
|
|
48
|
+
const active = useActiveMatch({ to: parent, activeOnlyWhenExact, activeWhenMatch });
|
|
53
49
|
|
|
54
50
|
useEffect(() => {
|
|
55
51
|
if (active) {
|
|
@@ -14,35 +14,33 @@
|
|
|
14
14
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
import { useLocation, useRouteMatch } from "react-router-dom";
|
|
17
|
+
import { useLocation, useResolvedPath } from "react-router-dom";
|
|
19
18
|
import { RoutingProps } from "./RoutingProps";
|
|
20
19
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
path = to.substr(0, index);
|
|
20
|
+
const useMatchesRoute = (pattern?: string) => {
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
if (!pattern) {
|
|
23
|
+
return false;
|
|
26
24
|
}
|
|
25
|
+
const regex = new RegExp(pattern);
|
|
26
|
+
return regex.test(location.pathname || "");
|
|
27
|
+
};
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
path: urls.escapeUrlForRoute(path),
|
|
30
|
-
exact: activeOnlyWhenExact,
|
|
31
|
-
});
|
|
32
|
-
|
|
29
|
+
const useActiveMatch = ({ to, activeWhenMatch, activeOnlyWhenExact = true }: RoutingProps) => {
|
|
33
30
|
const location = useLocation();
|
|
31
|
+
const resolved = useResolvedPath(to);
|
|
32
|
+
const isActive = new RegExp(resolved.pathname).test(location.pathname);
|
|
33
|
+
const match = useMatchesRoute(activeWhenMatch);
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
35
|
+
if (activeOnlyWhenExact) {
|
|
36
|
+
return isActive;
|
|
37
|
+
} else if (activeWhenMatch) {
|
|
38
|
+
if (isActive) {
|
|
39
|
+
return true;
|
|
41
40
|
}
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return !!match || isActiveWhenMatch();
|
|
41
|
+
return match;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
46
44
|
};
|
|
47
45
|
|
|
48
46
|
export default useActiveMatch;
|