@scm-manager/ui-core 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 (57) hide show
  1. package/{src/base/buttons/a11y.test.ts → .storybook/i18n.ts} +27 -9
  2. package/.storybook/main.ts +54 -0
  3. package/.storybook/preview-head.html +6 -1
  4. package/.storybook/preview.tsx +125 -0
  5. package/.turbo/turbo-test.log +164 -0
  6. package/.turbo/turbo-typecheck.log +3 -2
  7. package/package.json +36 -41
  8. package/src/base/buttons/Button.stories.tsx +179 -70
  9. package/src/base/buttons/Button.tsx +9 -9
  10. package/src/base/forms/AddListEntryForm.tsx +8 -8
  11. package/src/base/forms/ConfigurationForm.tsx +14 -5
  12. package/src/base/forms/Form.stories.tsx +599 -289
  13. package/src/base/forms/Form.tsx +8 -8
  14. package/src/base/forms/FormPathContext.tsx +3 -3
  15. package/src/base/forms/ScmFormContext.tsx +7 -4
  16. package/src/base/forms/ScmFormListContext.tsx +2 -1
  17. package/src/base/forms/base/Field.tsx +1 -1
  18. package/src/base/forms/base/label/Label.tsx +1 -1
  19. package/src/base/forms/chip-input/ChipInputField.stories.tsx +109 -28
  20. package/src/base/forms/chip-input/ChipInputField.tsx +20 -8
  21. package/src/base/forms/chip-input/ControlledChipInputField.tsx +3 -1
  22. package/src/base/forms/combobox/Combobox.stories.tsx +216 -89
  23. package/src/base/forms/combobox/Combobox.tsx +4 -2
  24. package/src/base/forms/combobox/ComboboxField.tsx +2 -1
  25. package/src/base/forms/headless-chip-input/ChipInput.tsx +9 -9
  26. package/src/base/forms/helpers.ts +12 -9
  27. package/src/base/forms/input/ControlledSecretConfirmationField.tsx +4 -2
  28. package/src/base/forms/radio-button/RadioButton.stories.tsx +317 -124
  29. package/src/base/forms/radio-button/RadioButton.tsx +8 -4
  30. package/src/base/forms/radio-button/RadioButtonContext.tsx +2 -1
  31. package/src/base/forms/table/ControlledColumn.tsx +1 -1
  32. package/src/base/forms/table/ControlledTable.tsx +12 -4
  33. package/src/base/helpers/useDocumentTitle.test.ts +15 -7
  34. package/src/base/layout/card/Card.stories.tsx +171 -72
  35. package/src/base/layout/card/Card.tsx +4 -4
  36. package/src/base/layout/card/CardDetail.tsx +2 -3
  37. package/src/base/layout/card-list/CardList.stories.tsx +283 -169
  38. package/src/base/layout/collapsible/Collapsible.stories.tsx +54 -16
  39. package/src/base/layout/index.ts +2 -5
  40. package/src/base/layout/tabs/Tabs.stories.tsx +58 -16
  41. package/src/base/layout/templates/data-page/DataPage.stories.tsx +289 -156
  42. package/src/base/layout/templates/data-page/DataPageHeader.tsx +1 -1
  43. package/src/base/overlays/dialog/Dialog.stories.tsx +94 -34
  44. package/src/base/overlays/menu/Menu.stories.tsx +116 -48
  45. package/src/base/overlays/menu/Menu.tsx +1 -0
  46. package/src/base/overlays/popover/Popover.stories.tsx +50 -37
  47. package/src/base/shortcuts/iterator/keyboardIterator.test.tsx +16 -7
  48. package/src/base/shortcuts/iterator/keyboardIterator.tsx +13 -5
  49. package/src/base/shortcuts/useShortcutDocs.tsx +2 -3
  50. package/src/base/status/StatusIcon.stories.tsx +76 -27
  51. package/src/base/status/index.ts +1 -1
  52. package/src/base/text/SplitAndReplace.stories.tsx +128 -50
  53. package/src/base/text/index.ts +1 -1
  54. package/.storybook/RemoveThemesPlugin.js +0 -49
  55. package/.storybook/main.js +0 -86
  56. package/.storybook/preview.js +0 -87
  57. package/src/base/buttons/image-snapshot.test.ts +0 -26
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import React, { FC, useCallback, useEffect, useState } from "react";
18
- import { DeepPartial, SubmitHandler, useForm, UseFormReturn } from "react-hook-form";
18
+ import { DefaultValues, SubmitHandler, useForm, UseFormReturn } from "react-hook-form";
19
19
  import { ErrorNotification } from "../notifications";
20
20
  import { Level } from "../misc";
21
21
  import { ScmFormContextProvider } from "./ScmFormContext";
@@ -47,11 +47,11 @@ const SuccessNotification: FC<{ label?: string; hide: () => void }> = ({ label,
47
47
  );
48
48
  };
49
49
 
50
- type Props<FormType extends Record<string, unknown>, DefaultValues extends FormType> = {
50
+ type Props<FormType extends Record<string, unknown>> = {
51
51
  children: ((renderProps: RenderProps<FormType>) => React.ReactNode | React.ReactNode[]) | React.ReactNode;
52
52
  translationPath: [namespace: string, prefix: string];
53
53
  onSubmit: SubmitHandler<FormType>;
54
- defaultValues: DefaultValues;
54
+ defaultValues: DefaultValues<FormType>;
55
55
  readOnly?: boolean;
56
56
  submitButtonTestId?: string;
57
57
  /**
@@ -73,7 +73,7 @@ type Props<FormType extends Record<string, unknown>, DefaultValues extends FormT
73
73
  *
74
74
  * @since 2.43.0
75
75
  */
76
- withResetTo?: DefaultValues;
76
+ withResetTo?: DefaultValues<FormType>;
77
77
  /**
78
78
  * Message to display after a successful submit if no translation key is defined.
79
79
  *
@@ -89,7 +89,7 @@ type Props<FormType extends Record<string, unknown>, DefaultValues extends FormT
89
89
  * @beta
90
90
  * @since 2.41.0
91
91
  */
92
- function Form<FormType extends Record<string, unknown>, DefaultValues extends FormType = FormType>({
92
+ function Form<FormType extends Record<string, unknown>>({
93
93
  children,
94
94
  onSubmit,
95
95
  defaultValues,
@@ -99,10 +99,10 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
99
99
  withDiscardChanges,
100
100
  successMessageFallback,
101
101
  submitButtonTestId,
102
- }: Props<FormType, DefaultValues>) {
102
+ }: Props<FormType>) {
103
103
  const form = useForm<FormType>({
104
104
  mode: "onChange",
105
- defaultValues: defaultValues as DeepPartial<FormType>,
105
+ defaultValues: defaultValues,
106
106
  });
107
107
  const { formState, handleSubmit, reset, setValue } = form;
108
108
  const [ns, prefix] = translationPath;
@@ -149,7 +149,7 @@ function Form<FormType extends Record<string, unknown>, DefaultValues extends Fo
149
149
  }, [isDirty]);
150
150
 
151
151
  const submit = useCallback(
152
- async (data) => {
152
+ async (data: any) => {
153
153
  setError(null);
154
154
  try {
155
155
  return await onSubmit(data);
@@ -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, useMemo } from "react";
17
+ import React, { FC, ReactNode, useContext, useMemo } from "react";
18
18
 
19
19
  const ScmFormPathContext = React.createContext<string>("");
20
20
 
@@ -22,7 +22,7 @@ export function useScmFormPathContext() {
22
22
  return useContext(ScmFormPathContext);
23
23
  }
24
24
 
25
- export const ScmFormPathContextProvider: FC<{ path: string }> = ({ children, path }) => (
25
+ export const ScmFormPathContextProvider: FC<{ path: string; children?: ReactNode }> = ({ children, path }) => (
26
26
  <ScmFormPathContext.Provider value={path}>{children}</ScmFormPathContext.Provider>
27
27
  );
28
28
 
@@ -58,7 +58,7 @@ export const ScmFormPathContextProvider: FC<{ path: string }> = ({ children, pat
58
58
  * // This pattern becomes useful in complex or large forms.
59
59
  * ```
60
60
  */
61
- export const ScmNestedFormPathContextProvider: FC<{ path: string }> = ({ children, path }) => {
61
+ export const ScmNestedFormPathContextProvider: FC<{ path: string; children?: ReactNode }> = ({ children, path }) => {
62
62
  const prefix = useScmFormPathContext();
63
63
  const pathWithPrefix = useMemo(() => (prefix ? `${prefix}.${path}` : path), [path, prefix]);
64
64
  return <ScmFormPathContext.Provider value={pathWithPrefix}>{children}</ScmFormPathContext.Provider>;
@@ -15,18 +15,21 @@
15
15
  */
16
16
 
17
17
  import React, { PropsWithChildren, useContext } from "react";
18
- import { UseFormReturn } from "react-hook-form";
18
+ import { FieldValues, UseFormReturn } from "react-hook-form";
19
19
  import type { TFunction } from "i18next";
20
20
 
21
- type ContextType<T = any> = UseFormReturn<T> & {
21
+ type ContextType<T extends FieldValues> = UseFormReturn<T> & {
22
22
  t: TFunction;
23
23
  readOnly?: boolean;
24
24
  formId: string;
25
25
  };
26
26
 
27
- const ScmFormContext = React.createContext<ContextType>(null as unknown as ContextType);
27
+ const ScmFormContext = React.createContext<ContextType<any>>(null as any);
28
28
 
29
- export function ScmFormContextProvider<T>({ children, ...props }: PropsWithChildren<ContextType<T>>) {
29
+ export function ScmFormContextProvider<T extends FieldValues>({
30
+ children,
31
+ ...props
32
+ }: PropsWithChildren<ContextType<T>>) {
30
33
  return <ScmFormContext.Provider value={props}>{children}</ScmFormContext.Provider>;
31
34
  }
32
35
 
@@ -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, useMemo } from "react";
17
+ import React, { FC, ReactNode, useContext, useMemo } from "react";
18
18
  import { FieldValues, useFieldArray, UseFieldArrayReturn } from "react-hook-form";
19
19
  import { ScmFormPathContextProvider, useScmFormPathContext } from "./FormPathContext";
20
20
  import { useScmFormContext } from "./ScmFormContext";
@@ -25,6 +25,7 @@ const ScmFormListContext = React.createContext<ContextType>(null as unknown as C
25
25
 
26
26
  type Props = {
27
27
  name: string;
28
+ children?: ReactNode;
28
29
  };
29
30
 
30
31
  /**
@@ -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, HTMLProps } from "react";
17
+ import React, { FC, HTMLProps, JSX } from "react";
18
18
  import classNames from "classnames";
19
19
 
20
20
  const Field: FC<HTMLProps<HTMLDivElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
@@ -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, HTMLProps } from "react";
17
+ import React, { FC, HTMLProps, JSX } from "react";
18
18
  import classNames from "classnames";
19
19
 
20
20
  const Label: FC<HTMLProps<HTMLLabelElement> | ({ as: keyof JSX.IntrinsicElements } & HTMLProps<HTMLElement>)> = ({
@@ -14,37 +14,95 @@
14
14
  * along with this program. If not, see https://www.gnu.org/licenses/.
15
15
  */
16
16
 
17
- import { storiesOf } from "@storybook/react";
17
+ // import { storiesOf } from "@storybook/react";
18
+ // import React, { useRef, useState } from "react";
19
+ // import ChipInputField from "./ChipInputField";
20
+ // import Combobox from "../combobox/Combobox";
21
+ // import { Option } from "@scm-manager/ui-types";
22
+ // import ChipInput from "../headless-chip-input/ChipInput";
23
+ //
24
+ // storiesOf("Chip Input Field", module)
25
+ // .add("Default", () => {
26
+ // const [value, setValue] = useState<Option<string>[]>([]);
27
+ // const ref = useRef<HTMLInputElement>(null);
28
+ // return (
29
+ // <>
30
+ // <ChipInputField
31
+ // value={value}
32
+ // onChange={setValue}
33
+ // label="Test Chips"
34
+ // placeholder="Type a new chip name and press enter to add"
35
+ // aria-label="My personal chip list"
36
+ // ref={ref}
37
+ // />
38
+ // <ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
39
+ // </>
40
+ // );
41
+ // })
42
+ // .add("With Autocomplete", () => {
43
+ // const people = ["Durward Reynolds", "Kenton Towne", "Therese Wunsch", "Benedict Kessler", "Katelyn Rohan"];
44
+ //
45
+ // const [value, setValue] = useState<Option<string>[]>([]);
46
+ //
47
+ // return (
48
+ // <ChipInputField
49
+ // value={value}
50
+ // onChange={setValue}
51
+ // label="Persons"
52
+ // placeholder="Enter a new person"
53
+ // aria-label="Enter a new person"
54
+ // >
55
+ // <Combobox
56
+ // options={(query: string) =>
57
+ // Promise.resolve(
58
+ // people
59
+ // .map<Option<string>>((p) => ({ label: p, value: p }))
60
+ // .filter((t) => !value.some((val) => val.label === t.label) && t.label.startsWith(query))
61
+ // .concat({ label: query, value: query, displayValue: `Use '${query}'` })
62
+ // )
63
+ // }
64
+ // />
65
+ // </ChipInputField>
66
+ // );
67
+ // });
68
+
18
69
  import React, { useRef, useState } from "react";
19
- import ChipInputField from "./ChipInputField";
20
- import Combobox from "../combobox/Combobox";
70
+ import type { Meta, StoryObj } from "@storybook/react";
21
71
  import { Option } from "@scm-manager/ui-types";
72
+
73
+ import ChipInputField from "./ChipInputField";
22
74
  import ChipInput from "../headless-chip-input/ChipInput";
75
+ import Combobox from "../combobox/Combobox";
23
76
 
24
- storiesOf("Chip Input Field", module)
25
- .add("Default", () => {
26
- const [value, setValue] = useState<Option<string>[]>([]);
27
- const ref = useRef<HTMLInputElement>(null);
28
- return (
29
- <>
30
- <ChipInputField
31
- value={value}
32
- onChange={setValue}
33
- label="Test Chips"
34
- placeholder="Type a new chip name and press enter to add"
35
- aria-label="My personal chip list"
36
- ref={ref}
37
- />
38
- <ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
39
- </>
40
- );
41
- })
42
- .add("With Autocomplete", () => {
43
- const people = ["Durward Reynolds", "Kenton Towne", "Therese Wunsch", "Benedict Kessler", "Katelyn Rohan"];
77
+ const DefaultExample = () => {
78
+ const [value, setValue] = useState<Option<string>[]>([]);
79
+ const ref = useRef<HTMLInputElement>(null);
44
80
 
45
- const [value, setValue] = useState<Option<string>[]>([]);
81
+ return (
82
+ <>
83
+ {/* @ts-ignore */}
84
+ <ChipInputField
85
+ value={value}
86
+ onChange={setValue}
87
+ label="Test Chips"
88
+ placeholder="Type a new chip name and press enter to add"
89
+ aria-label="My personal chip list"
90
+ ref={ref}
91
+ />
92
+ <ChipInput.AddButton inputRef={ref} className="button is-link is-outlined ml-1 mt-2">
93
+ Add
94
+ </ChipInput.AddButton>
95
+ </>
96
+ );
97
+ };
98
+
99
+ const WithAutocompleteExample = () => {
100
+ const people = ["Durward Reynolds", "Kenton Towne", "Therese Wunsch", "Benedict Kessler", "Katelyn Rohan"];
101
+ const [value, setValue] = useState<Option<string>[]>([]);
46
102
 
47
- return (
103
+ return (
104
+ <>
105
+ {/* @ts-ignore */}
48
106
  <ChipInputField
49
107
  value={value}
50
108
  onChange={setValue}
@@ -52,16 +110,39 @@ storiesOf("Chip Input Field", module)
52
110
  placeholder="Enter a new person"
53
111
  aria-label="Enter a new person"
54
112
  >
113
+ {/* @ts-ignore */}
55
114
  <Combobox
56
115
  options={(query: string) =>
57
116
  Promise.resolve(
58
117
  people
59
118
  .map<Option<string>>((p) => ({ label: p, value: p }))
60
119
  .filter((t) => !value.some((val) => val.label === t.label) && t.label.startsWith(query))
61
- .concat({ label: query, value: query, displayValue: `Use '${query}'` })
120
+ .concat({ label: query, value: query, displayValue: `Use '${query}'` }),
62
121
  )
63
122
  }
64
123
  />
65
124
  </ChipInputField>
66
- );
67
- });
125
+ </>
126
+ );
127
+ };
128
+
129
+ const meta: Meta<typeof ChipInputField> = {
130
+ title: "Forms/ChipInputField",
131
+
132
+ // @ts-ignore
133
+ component: ChipInputField,
134
+ decorators: [(Story) => <div style={{ margin: "2rem", maxWidth: "40rem" }}>{Story()}</div>],
135
+ tags: ["autodocs"],
136
+ };
137
+
138
+ export default meta;
139
+
140
+ type Story = StoryObj<typeof meta>;
141
+
142
+ export const Default: Story = {
143
+ render: () => <DefaultExample />,
144
+ };
145
+
146
+ export const WithAutocomplete: Story = {
147
+ render: () => <WithAutocompleteExample />,
148
+ };
@@ -29,7 +29,17 @@ import { useTranslation } from "react-i18next";
29
29
  import { withForwardRef } from "../helpers";
30
30
  import { Option } from "@scm-manager/ui-types";
31
31
 
32
- const StyledChipInput: typeof ChipInput = styled(ChipInput)`
32
+ const ChipInputWrapper = React.forwardRef<HTMLUListElement, any>((props, ref) => (
33
+ // @ts-ignore
34
+ <ChipInput ref={ref} {...props} />
35
+ ));
36
+
37
+ const NewChipInputWrapper = React.forwardRef<HTMLUListElement, any>((props, ref) => (
38
+ // @ts-ignore
39
+ <NewChipInput ref={ref} {...props} />
40
+ ));
41
+
42
+ const StyledChipInput = styled(ChipInputWrapper)`
33
43
  min-height: 40px;
34
44
  height: min-content;
35
45
  gap: 0.5rem;
@@ -39,7 +49,7 @@ const StyledChipInput: typeof ChipInput = styled(ChipInput)`
39
49
  }
40
50
  `;
41
51
 
42
- const StyledInput = styled(NewChipInput)`
52
+ const StyledInput = styled(NewChipInputWrapper)`
43
53
  color: var(--scm-secondary-more-color);
44
54
  font-size: 1rem;
45
55
  height: initial;
@@ -48,7 +58,7 @@ const StyledInput = styled(NewChipInput)`
48
58
  &:focus {
49
59
  outline: none;
50
60
  }
51
- ` as unknown as typeof NewChipInput;
61
+ `;
52
62
 
53
63
  const StyledDelete = styled(ChipInput.Chip.Delete)`
54
64
  &:focus {
@@ -113,17 +123,18 @@ const ChipInputField = function ChipInputField<T>(
113
123
  isNewItemDuplicate,
114
124
  ...props
115
125
  }: PropsWithRef<InputFieldProps<T>>,
116
- ref: React.ForwardedRef<HTMLInputElement>
126
+ ref: React.ForwardedRef<HTMLInputElement>,
117
127
  ) {
118
128
  const [t] = useTranslation("commons", { keyPrefix: "form.chipList" });
119
129
  const deleteTextCallback = useCallback(
120
- (item) => (createDeleteText ? createDeleteText(item) : t("delete", { item })),
121
- [createDeleteText, t]
130
+ (item: any) => (createDeleteText ? createDeleteText(item) : t("delete", { item })),
131
+ [createDeleteText, t],
122
132
  );
123
133
  const inputId = useAriaId(id ?? testId);
124
134
  const labelId = useAriaId();
125
135
  const inputDescriptionId = useAriaId();
126
136
  const variant = determineVariant(error, warning);
137
+
127
138
  return (
128
139
  <Field className={className} aria-owns={inputId}>
129
140
  <Label id={labelId}>
@@ -134,7 +145,7 @@ const ChipInputField = function ChipInputField<T>(
134
145
  <div className={classNames("control", { "is-loading": isLoading })}>
135
146
  <StyledChipInput
136
147
  value={value}
137
- onChange={(e) => onChange && onChange(e ?? [])}
148
+ onChange={(e: any) => onChange && onChange(e ?? [])}
138
149
  className="is-flex is-flex-wrap-wrap input"
139
150
  aria-labelledby={labelId}
140
151
  disabled={readOnly || disabled}
@@ -154,10 +165,11 @@ const ChipInputField = function ChipInputField<T>(
154
165
  "is-shadowless",
155
166
  "input",
156
167
  "is-ellipsis-overflow",
157
- createVariantClass(variant)
168
+ createVariantClass(variant),
158
169
  )}
159
170
  placeholder={!readOnly && !disabled ? placeholder : ""}
160
171
  id={inputId}
172
+ // @ts-ignore
161
173
  ref={ref}
162
174
  aria-describedby={inputDescriptionId}
163
175
  {...createAttributesForTesting(testId)}
@@ -56,7 +56,7 @@ function ControlledChipInputField<T extends Record<string, unknown>>(
56
56
  optionFactory = defaultOptionFactory,
57
57
  ...props
58
58
  }: Props<T>,
59
- ref: React.ForwardedRef<HTMLInputElement>
59
+ ref: React.ForwardedRef<HTMLInputElement>,
60
60
  ) {
61
61
  const { control, t, readOnly: formReadonly } = useScmFormContext();
62
62
  const formPathPrefix = useScmFormPathContext();
@@ -67,6 +67,7 @@ function ControlledChipInputField<T extends Record<string, unknown>>(
67
67
  const placeholderTranslation = placeholder || t(`${prefixedNameWithoutIndices}.placeholder`) || "";
68
68
  const ariaLabelTranslation = t(`${prefixedNameWithoutIndices}.ariaLabel`);
69
69
  const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
70
+
70
71
  return (
71
72
  <Controller
72
73
  control={control}
@@ -74,6 +75,7 @@ function ControlledChipInputField<T extends Record<string, unknown>>(
74
75
  rules={rules}
75
76
  defaultValue={defaultValue}
76
77
  render={({ field: { value, onChange, ...field }, fieldState }) => (
78
+ // @ts-ignore
77
79
  <ChipInputField
78
80
  label={labelTranslation}
79
81
  helpText={helpTextTranslation}