@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.
Files changed (130) hide show
  1. package/package.json +45 -50
  2. package/src/BranchSelector.stories.tsx +60 -11
  3. package/src/Breadcrumb.stories.tsx +131 -57
  4. package/src/Breadcrumb.tsx +3 -3
  5. package/src/CardColumn.stories.tsx +94 -27
  6. package/src/CardColumnSmall.stories.tsx +102 -27
  7. package/src/CommaSeparatedList.tsx +2 -2
  8. package/src/Date.stories.tsx +64 -17
  9. package/src/Duration.stories.tsx +92 -45
  10. package/src/ErrorBoundary.tsx +38 -58
  11. package/src/ErrorNotification.tsx +1 -1
  12. package/src/Help.stories.tsx +61 -17
  13. package/src/Help.tsx +5 -11
  14. package/src/Icon.stories.tsx +93 -13
  15. package/src/Image.tsx +2 -3
  16. package/src/LinkPaginator.tsx +9 -6
  17. package/src/Loading.stories.tsx +26 -6
  18. package/src/Logo.stories.tsx +44 -13
  19. package/src/Notification.stories.tsx +111 -23
  20. package/src/OverviewPageActions.tsx +5 -5
  21. package/src/Paginator.test.tsx +115 -94
  22. package/src/PdfViewer.stories.tsx +83 -5
  23. package/src/PreformattedCodeBlock.stories.tsx +97 -26
  24. package/src/ProtectedRoute.tsx +14 -39
  25. package/src/SmallLoadingSpinner.stories.tsx +26 -6
  26. package/src/SyntaxHighlighter.stories.tsx +122 -40
  27. package/src/SyntaxHighlighterRenderer.tsx +7 -7
  28. package/src/Tag.stories.tsx +135 -18
  29. package/src/Tag.tsx +2 -1
  30. package/src/Tooltip.stories.tsx +147 -34
  31. package/src/__snapshots__/storyshots.test.ts.snap +21 -144
  32. package/src/avatar/Avatar.ts +1 -1
  33. package/src/avatar/AvatarImage.tsx +2 -2
  34. package/src/avatar/AvatarWrapper.tsx +2 -2
  35. package/src/buttons/Button.stories.tsx +159 -0
  36. package/src/buttons/Button.tsx +5 -5
  37. package/src/config/ConfigurationBinder.tsx +39 -39
  38. package/src/config/ConfigurationForm.tsx +2 -1
  39. package/src/config/TitledSettings.tsx +4 -1
  40. package/src/forms/AddKeyValueEntryToTableField.stories.tsx +60 -25
  41. package/src/forms/Checkbox.stories.tsx +165 -27
  42. package/src/forms/Checkbox.tsx +1 -0
  43. package/src/forms/DropDown.stories.tsx +81 -35
  44. package/src/forms/FileInput.stories.tsx +85 -10
  45. package/src/forms/InputField.stories.tsx +210 -37
  46. package/src/forms/InputField.tsx +1 -0
  47. package/src/forms/Radio.stories.tsx +181 -34
  48. package/src/forms/Radio.tsx +1 -0
  49. package/src/forms/Select.stories.tsx +266 -46
  50. package/src/forms/Select.tsx +1 -0
  51. package/src/forms/Textarea.stories.tsx +99 -53
  52. package/src/forms/Textarea.tsx +1 -0
  53. package/src/forms/index.ts +3 -1
  54. package/src/index.ts +5 -5
  55. package/src/jest-dom.d.ts +17 -0
  56. package/src/layout/Footer.stories.tsx +114 -22
  57. package/src/layout/Footer.tsx +11 -7
  58. package/src/layout/FooterSection.tsx +1 -0
  59. package/src/layout/Header.tsx +2 -1
  60. package/src/layout/Page.tsx +1 -3
  61. package/src/layout/PrimaryContentColumn.tsx +2 -2
  62. package/src/layout/SecondaryNavigationColumn.tsx +3 -3
  63. package/src/layout/SubSubtitle.tsx +2 -1
  64. package/src/layout/Subtitle.tsx +2 -1
  65. package/src/layout/Title.tsx +2 -5
  66. package/src/markdown/LazyMarkdownView.tsx +16 -5
  67. package/src/markdown/MarkdownHeadingRenderer.test.ts +9 -1
  68. package/src/markdown/MarkdownHeadingRenderer.tsx +3 -3
  69. package/src/markdown/MarkdownImageRenderer.test.ts +8 -1
  70. package/src/markdown/MarkdownImageRenderer.tsx +2 -1
  71. package/src/markdown/MarkdownLinkRenderer.test.tsx +9 -0
  72. package/src/markdown/MarkdownLinkRenderer.tsx +6 -4
  73. package/src/markdown/MarkdownView.stories.tsx +224 -72
  74. package/src/markdown/markdownExtensions.ts +6 -4
  75. package/src/modals/ActiveModalCountContextProvider.tsx +2 -2
  76. package/src/modals/ConfirmAlert.stories.tsx +133 -44
  77. package/src/modals/ConfirmAlert.tsx +5 -4
  78. package/src/modals/Modal.stories.tsx +461 -252
  79. package/src/modals/Modal.tsx +12 -11
  80. package/src/modals/useRegisterModal.test.tsx +5 -4
  81. package/src/navigation/MenuContext.tsx +2 -2
  82. package/src/navigation/NavLink.tsx +4 -7
  83. package/src/navigation/PrimaryNavigation.tsx +2 -2
  84. package/src/navigation/PrimaryNavigationLink.tsx +6 -5
  85. package/src/navigation/RoutingProps.ts +1 -3
  86. package/src/navigation/SecondaryNavigation.stories.tsx +157 -45
  87. package/src/navigation/SecondaryNavigation.tsx +17 -16
  88. package/src/navigation/SecondaryNavigationItem.tsx +2 -1
  89. package/src/navigation/SubNavigation.tsx +4 -8
  90. package/src/navigation/useActiveMatch.ts +20 -22
  91. package/src/navigation/useNavigationLock.ts +1 -0
  92. package/src/popover/Popover.stories.tsx +111 -41
  93. package/src/popover/Popover.tsx +2 -5
  94. package/src/repos/Diff.stories.tsx +418 -223
  95. package/src/repos/Diff.tsx +1 -5
  96. package/src/repos/DiffTypes.ts +2 -14
  97. package/src/repos/HunkExpandDivider.tsx +2 -2
  98. package/src/repos/LoadingDiff.tsx +11 -6
  99. package/src/repos/RepositoryEntry.stories.tsx +217 -53
  100. package/src/repos/RepositoryFlag.tsx +2 -1
  101. package/src/repos/TokenizedDiffView.tsx +4 -2
  102. package/src/repos/annotate/Annotate.stories.tsx +225 -111
  103. package/src/repos/annotate/AnnotateLine.tsx +2 -1
  104. package/src/repos/changesets/ChangesetAuthor.tsx +4 -4
  105. package/src/repos/changesets/ChangesetButtonGroup.test.tsx +9 -5
  106. package/src/repos/changesets/ChangesetDiff.test.ts +10 -2
  107. package/src/repos/changesets/Changesets.stories.tsx +388 -197
  108. package/src/repos/changesets/Contributor.tsx +1 -1
  109. package/src/repos/changesets/ContributorRow.tsx +2 -2
  110. package/src/repos/changesets/SignatureIcon.tsx +1 -0
  111. package/src/repos/changesets/SingleChangeset.tsx +0 -1
  112. package/src/repos/diff/DiffFileTree.tsx +37 -89
  113. package/src/repos/diff/styledElements.tsx +4 -3
  114. package/src/repos/index.ts +15 -15
  115. package/src/search/Hit.tsx +3 -2
  116. package/src/search/TextHitField.stories.tsx +131 -43
  117. package/src/search/TextHitField.tsx +3 -2
  118. package/src/search/index.ts +1 -1
  119. package/src/storyshots.test.ts +66 -60
  120. package/src/table/Table.stories.tsx +146 -48
  121. package/src/table/Table.tsx +7 -8
  122. package/src/table/TextColumn.tsx +0 -9
  123. package/src/toast/Toast.tsx +2 -1
  124. package/src/toast/ToastArea.tsx +2 -2
  125. package/src/toast/ToastButton.tsx +2 -1
  126. package/src/toast/ToastButtons.tsx +4 -2
  127. package/src/toast/ToastNotification.tsx +2 -1
  128. package/src/toast/index.stories.tsx +144 -39
  129. package/src/toast/index.ts +1 -1
  130. package/src/buttons/index.stories.tsx +0 -85
@@ -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
- className
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
- <Dialog.Title as="header" className={classNames("modal-card-head", `has-background-${headColor}`)}>
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
- </Dialog.Title>
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-hooks";
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, activeWhenMatch, activeOnlyWhenExact, title, testId, children, ...contentProps }) => {
51
- const active = useActiveMatch({ to, activeWhenMatch, activeOnlyWhenExact });
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 { useRouteMatch, Link } from "react-router-dom";
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 routeMatch = useRouteMatch({ path: match });
31
+ const location = useLocation();
32
+ const isActive = new RegExp(match).test(location.pathname);
32
33
 
33
34
  return (
34
- <Link
35
+ <NavLink
35
36
  to={to}
36
- className={classNames(className, "navbar-item", { "is-active": routeMatch })}
37
+ className={() => classNames(className, "navbar-item", { "is-active": isActive })}
37
38
  {...createAttributesForTesting(testId)}
38
39
  >
39
40
  {label}
40
- </Link>
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?: (route: RouteProps) => boolean;
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
- const withRoute = (route: string) => {
44
- return (story: ReactElement<any>) => <MemoryRouter initialEntries={[route]}>{story}</MemoryRouter>;
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
- 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", () => {
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 withRoute("/hitchhiker/starships/titanic")(
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
- .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
- <SecondaryNavigationItem
88
- activeWhenMatch={(route) => route.location?.pathname === "/hog"}
89
- to="/heart-of-gold"
90
- icon="fas fa-star"
91
- label="Heart Of Gold"
92
- title="Heart Of Gold"
93
- />
94
- </SecondaryNavigation>
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 MenuButton = styled(Button)<CollapsedProps>`
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(collapsible);
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" collapsed={collapsed} onClick={toggleCollapse} aria-label={menuAriaLabel}>
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 { urls } from "@scm-manager/ui-api";
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 useActiveMatch = ({ to, activeOnlyWhenExact, activeWhenMatch }: RoutingProps) => {
22
- let path = to;
23
- const index = to.indexOf("?");
24
- if (index > 0) {
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
- const match = useRouteMatch({
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
- const isActiveWhenMatch = () => {
36
- if (activeWhenMatch) {
37
- // noinspection PointlessBooleanExpressionJS could be a truthy value if a function is passed in
38
- return !!activeWhenMatch({
39
- location,
40
- });
35
+ if (activeOnlyWhenExact) {
36
+ return isActive;
37
+ } else if (activeWhenMatch) {
38
+ if (isActive) {
39
+ return true;
41
40
  }
42
- return false;
43
- };
44
-
45
- return !!match || isActiveWhenMatch();
41
+ return match;
42
+ }
43
+ return false;
46
44
  };
47
45
 
48
46
  export default useActiveMatch;
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { useEffect } from "react";
18
18
 
19
+ // TODO: Navigation
19
20
  // This hook can be used to warn the user on reloading or closing the current page if the navigation lock is enabled.
20
21
  const useNavigationLock = (enabled: boolean) => {
21
22
  useEffect(() => {