@scm-manager/ui-core 3.0.0-20240024-101702

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 (128) hide show
  1. package/.storybook/.babelrc +3 -0
  2. package/.storybook/RemoveThemesPlugin.js +57 -0
  3. package/.storybook/main.js +92 -0
  4. package/.storybook/preview-head.html +25 -0
  5. package/.storybook/preview.js +95 -0
  6. package/.storybook/withApiProvider.js +46 -0
  7. package/docs/introduction.stories.mdx +64 -0
  8. package/docs/usage.stories.mdx +22 -0
  9. package/package.json +81 -0
  10. package/src/base/buttons/Button.stories.tsx +89 -0
  11. package/src/base/buttons/Button.test.stories.mdx +74 -0
  12. package/src/base/buttons/Button.tsx +143 -0
  13. package/src/base/buttons/Icon.tsx +58 -0
  14. package/src/base/buttons/a11y.test.ts +34 -0
  15. package/src/base/buttons/docs/introduction.stories.mdx +64 -0
  16. package/src/base/buttons/docs/usage.stories.mdx +22 -0
  17. package/src/base/buttons/image-snapshot.test.ts +33 -0
  18. package/src/base/buttons/index.ts +26 -0
  19. package/src/base/forms/AddListEntryForm.tsx +127 -0
  20. package/src/base/forms/ConfigurationForm.tsx +59 -0
  21. package/src/base/forms/Form.stories.tsx +453 -0
  22. package/src/base/forms/Form.tsx +215 -0
  23. package/src/base/forms/FormPathContext.tsx +73 -0
  24. package/src/base/forms/FormRow.tsx +37 -0
  25. package/src/base/forms/ScmFormContext.tsx +43 -0
  26. package/src/base/forms/ScmFormListContext.tsx +65 -0
  27. package/src/base/forms/base/Control.tsx +34 -0
  28. package/src/base/forms/base/Field.tsx +35 -0
  29. package/src/base/forms/base/field-message/FieldMessage.tsx +34 -0
  30. package/src/base/forms/base/help/Help.tsx +34 -0
  31. package/src/base/forms/base/label/Label.tsx +35 -0
  32. package/src/base/forms/checkbox/Checkbox.stories.mdx +26 -0
  33. package/src/base/forms/checkbox/Checkbox.tsx +118 -0
  34. package/src/base/forms/checkbox/CheckboxField.tsx +39 -0
  35. package/src/base/forms/checkbox/ControlledCheckboxField.stories.mdx +36 -0
  36. package/src/base/forms/checkbox/ControlledCheckboxField.tsx +82 -0
  37. package/src/base/forms/chip-input/ChipInputField.stories.tsx +75 -0
  38. package/src/base/forms/chip-input/ChipInputField.tsx +169 -0
  39. package/src/base/forms/chip-input/ControlledChipInputField.tsx +111 -0
  40. package/src/base/forms/combobox/Combobox.stories.tsx +125 -0
  41. package/src/base/forms/combobox/Combobox.tsx +223 -0
  42. package/src/base/forms/combobox/ComboboxField.tsx +62 -0
  43. package/src/base/forms/combobox/ControlledComboboxField.tsx +96 -0
  44. package/src/base/forms/headless-chip-input/ChipInput.tsx +237 -0
  45. package/src/base/forms/helpers.ts +74 -0
  46. package/src/base/forms/index.ts +85 -0
  47. package/src/base/forms/input/ControlledInputField.stories.mdx +36 -0
  48. package/src/base/forms/input/ControlledInputField.tsx +87 -0
  49. package/src/base/forms/input/ControlledSecretConfirmationField.stories.mdx +39 -0
  50. package/src/base/forms/input/ControlledSecretConfirmationField.tsx +138 -0
  51. package/src/base/forms/input/Input.stories.mdx +22 -0
  52. package/src/base/forms/input/Input.tsx +46 -0
  53. package/src/base/forms/input/InputField.stories.mdx +22 -0
  54. package/src/base/forms/input/InputField.tsx +61 -0
  55. package/src/base/forms/input/Textarea.stories.mdx +28 -0
  56. package/src/base/forms/input/Textarea.tsx +46 -0
  57. package/src/base/forms/list/ControlledList.tsx +88 -0
  58. package/src/base/forms/radio-button/ControlledRadioGroupField.tsx +94 -0
  59. package/src/base/forms/radio-button/RadioButton.stories.tsx +226 -0
  60. package/src/base/forms/radio-button/RadioButton.tsx +116 -0
  61. package/src/base/forms/radio-button/RadioButtonContext.tsx +42 -0
  62. package/src/base/forms/radio-button/RadioGroup.tsx +49 -0
  63. package/src/base/forms/radio-button/RadioGroupField.tsx +58 -0
  64. package/src/base/forms/resourceHooks.ts +164 -0
  65. package/src/base/forms/select/ControlledSelectField.tsx +87 -0
  66. package/src/base/forms/select/Select.tsx +57 -0
  67. package/src/base/forms/select/SelectField.tsx +63 -0
  68. package/src/base/forms/table/ControlledColumn.tsx +49 -0
  69. package/src/base/forms/table/ControlledTable.tsx +99 -0
  70. package/src/base/forms/variants.ts +27 -0
  71. package/src/base/helpers/devbuild.ts +44 -0
  72. package/src/base/helpers/index.ts +26 -0
  73. package/src/base/helpers/useAriaId.tsx +31 -0
  74. package/src/base/index.ts +34 -0
  75. package/src/base/layout/_helpers/with-classes.tsx +52 -0
  76. package/src/base/layout/card/Card.stories.tsx +113 -0
  77. package/src/base/layout/card/Card.tsx +76 -0
  78. package/src/base/layout/card/CardDetail.tsx +196 -0
  79. package/src/base/layout/card/CardRow.tsx +46 -0
  80. package/src/base/layout/card/CardTitle.tsx +59 -0
  81. package/src/base/layout/card-list/CardList.stories.tsx +201 -0
  82. package/src/base/layout/card-list/CardList.tsx +76 -0
  83. package/src/base/layout/collapsible/Collapsible.stories.tsx +45 -0
  84. package/src/base/layout/collapsible/Collapsible.tsx +87 -0
  85. package/src/base/layout/index.ts +93 -0
  86. package/src/base/layout/tabs/TabTrigger.tsx +46 -0
  87. package/src/base/layout/tabs/Tabs.stories.tsx +48 -0
  88. package/src/base/layout/tabs/Tabs.tsx +52 -0
  89. package/src/base/layout/tabs/TabsContent.tsx +33 -0
  90. package/src/base/layout/tabs/TabsList.tsx +41 -0
  91. package/src/base/layout/templates/data-page/DataPage.stories.tsx +201 -0
  92. package/src/base/layout/templates/data-page/DataPageHeader.tsx +100 -0
  93. package/src/base/misc/Image.tsx +32 -0
  94. package/src/base/misc/Level.tsx +40 -0
  95. package/src/base/misc/Loading.tsx +64 -0
  96. package/src/base/misc/SubSubtitle.tsx +36 -0
  97. package/src/base/misc/Subtitle.tsx +37 -0
  98. package/src/base/misc/Title.tsx +56 -0
  99. package/src/base/misc/index.ts +30 -0
  100. package/src/base/notifications/BackendErrorNotification.tsx +160 -0
  101. package/src/base/notifications/ErrorNotification.tsx +73 -0
  102. package/src/base/notifications/Notification.tsx +48 -0
  103. package/src/base/notifications/index.tsx +27 -0
  104. package/src/base/overlays/dialog/Dialog.stories.tsx +64 -0
  105. package/src/base/overlays/dialog/Dialog.tsx +85 -0
  106. package/src/base/overlays/index.ts +44 -0
  107. package/src/base/overlays/menu/Menu.stories.tsx +78 -0
  108. package/src/base/overlays/menu/Menu.tsx +213 -0
  109. package/src/base/overlays/menu/MenuTrigger.tsx +63 -0
  110. package/src/base/overlays/popover/Popover.stories.tsx +69 -0
  111. package/src/base/overlays/popover/Popover.tsx +95 -0
  112. package/src/base/overlays/tooltip/Tooltip.examples.js +41 -0
  113. package/src/base/overlays/tooltip/Tooltip.stories.mdx +52 -0
  114. package/src/base/overlays/tooltip/Tooltip.tsx +96 -0
  115. package/src/base/shortcuts/index.ts +28 -0
  116. package/src/base/shortcuts/iterator/callbackIterator.ts +220 -0
  117. package/src/base/shortcuts/iterator/keyboardIterator.test.tsx +431 -0
  118. package/src/base/shortcuts/iterator/keyboardIterator.tsx +141 -0
  119. package/src/base/shortcuts/usePauseShortcuts.ts +44 -0
  120. package/src/base/shortcuts/useShortcut.ts +110 -0
  121. package/src/base/shortcuts/useShortcutDocs.tsx +54 -0
  122. package/src/base/text/SplitAndReplace.stories.tsx +83 -0
  123. package/src/base/text/SplitAndReplace.tsx +65 -0
  124. package/src/base/text/index.ts +25 -0
  125. package/src/base/text/textSplitAndReplace.test.ts +134 -0
  126. package/src/base/text/textSplitAndReplace.ts +86 -0
  127. package/src/index.ts +25 -0
  128. package/tsconfig.json +6 -0
@@ -0,0 +1,82 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import React, { ComponentProps } from "react";
26
+ import { Controller, ControllerRenderProps, Path, RegisterOptions } from "react-hook-form";
27
+ import { useScmFormContext } from "../ScmFormContext";
28
+ import CheckboxField from "./CheckboxField";
29
+ import { useScmFormPathContext } from "../FormPathContext";
30
+ import { prefixWithoutIndices } from "../helpers";
31
+ import classNames from "classnames";
32
+
33
+ type Props<T extends Record<string, unknown>> = Omit<
34
+ ComponentProps<typeof CheckboxField>,
35
+ "label" | "defaultValue" | "required" | keyof ControllerRenderProps
36
+ > & {
37
+ name: Path<T>;
38
+ label?: string;
39
+ rules?: Pick<RegisterOptions, "deps">;
40
+ };
41
+
42
+ function ControlledInputField<T extends Record<string, unknown>>({
43
+ name,
44
+ label,
45
+ helpText,
46
+ rules,
47
+ testId,
48
+ defaultChecked,
49
+ readOnly,
50
+ className,
51
+ ...props
52
+ }: Props<T>) {
53
+ const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
54
+ const formPathPrefix = useScmFormPathContext();
55
+ const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
56
+ const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
57
+ const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
58
+ const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
59
+ return (
60
+ <Controller
61
+ control={control}
62
+ name={nameWithPrefix}
63
+ rules={rules}
64
+ defaultValue={defaultChecked as never}
65
+ render={({ field }) => (
66
+ <CheckboxField
67
+ form={formId}
68
+ readOnly={readOnly ?? formReadonly}
69
+ checked={field.value}
70
+ className={classNames("column", className)}
71
+ {...props}
72
+ {...field}
73
+ label={labelTranslation}
74
+ helpText={helpTextTranslation}
75
+ testId={testId ?? `checkbox-${nameWithPrefix}`}
76
+ />
77
+ )}
78
+ />
79
+ );
80
+ }
81
+
82
+ export default ControlledInputField;
@@ -0,0 +1,75 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { storiesOf } from "@storybook/react";
26
+ import React, { useRef, useState } from "react";
27
+ import ChipInputField from "./ChipInputField";
28
+ import Combobox from "../combobox/Combobox";
29
+ import { Option } from "@scm-manager/ui-types";
30
+ import ChipInput from "../headless-chip-input/ChipInput";
31
+
32
+ storiesOf("Chip Input Field", module)
33
+ .add("Default", () => {
34
+ const [value, setValue] = useState<Option<string>[]>([]);
35
+ const ref = useRef<HTMLInputElement>(null);
36
+ return (
37
+ <>
38
+ <ChipInputField
39
+ value={value}
40
+ onChange={setValue}
41
+ label="Test Chips"
42
+ placeholder="Type a new chip name and press enter to add"
43
+ aria-label="My personal chip list"
44
+ ref={ref}
45
+ />
46
+ <ChipInput.AddButton inputRef={ref}>Add</ChipInput.AddButton>
47
+ </>
48
+ );
49
+ })
50
+ .add("With Autocomplete", () => {
51
+ const people = ["Durward Reynolds", "Kenton Towne", "Therese Wunsch", "Benedict Kessler", "Katelyn Rohan"];
52
+
53
+ const [value, setValue] = useState<Option<string>[]>([]);
54
+
55
+ return (
56
+ <ChipInputField
57
+ value={value}
58
+ onChange={setValue}
59
+ label="Persons"
60
+ placeholder="Enter a new person"
61
+ aria-label="Enter a new person"
62
+ >
63
+ <Combobox
64
+ options={(query: string) =>
65
+ Promise.resolve(
66
+ people
67
+ .map<Option<string>>((p) => ({ label: p, value: p }))
68
+ .filter((t) => !value.some((val) => val.label === t.label) && t.label.startsWith(query))
69
+ .concat({ label: query, value: query, displayValue: `Use '${query}'` })
70
+ )
71
+ }
72
+ />
73
+ </ChipInputField>
74
+ );
75
+ });
@@ -0,0 +1,169 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import React, { KeyboardEventHandler, PropsWithRef, ReactElement, Ref, useCallback } from "react";
26
+ import { createAttributesForTesting, useAriaId } from "../../helpers";
27
+ import Field from "../base/Field";
28
+ import Label from "../base/label/Label";
29
+ import Help from "../base/help/Help";
30
+ import FieldMessage from "../base/field-message/FieldMessage";
31
+ import styled from "styled-components";
32
+ import classNames from "classnames";
33
+ import { createVariantClass } from "../variants";
34
+ import ChipInput, { NewChipInput } from "../headless-chip-input/ChipInput";
35
+ import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
36
+ import { useTranslation } from "react-i18next";
37
+ import { withForwardRef } from "../helpers";
38
+ import { Option } from "@scm-manager/ui-types";
39
+
40
+ const StyledChipInput: typeof ChipInput = styled(ChipInput)`
41
+ min-height: 40px;
42
+ height: min-content;
43
+ gap: 0.5rem;
44
+ &:focus-within {
45
+ border: 1px solid var(--scm-info-color);
46
+ box-shadow: rgba(51, 178, 232, 0.25) 0px 0px 0px 0.125em;
47
+ }
48
+ `;
49
+
50
+ const StyledInput = styled(NewChipInput)`
51
+ color: var(--scm-secondary-more-color);
52
+ font-size: 1rem;
53
+ height: initial;
54
+ padding: 0;
55
+ border-radius: 0;
56
+ &:focus {
57
+ outline: none;
58
+ }
59
+ ` as unknown as typeof NewChipInput;
60
+
61
+ const StyledDelete = styled(ChipInput.Chip.Delete)`
62
+ &:focus {
63
+ outline-offset: 0;
64
+ }
65
+ `;
66
+
67
+ type InputFieldProps<T> = {
68
+ label: string;
69
+ createDeleteText?: (value: string) => string;
70
+ helpText?: string;
71
+ error?: string;
72
+ testId?: string;
73
+ id?: string;
74
+ children?: ReactElement;
75
+ placeholder?: string;
76
+ onChange?: (newValue: Option<T>[]) => void;
77
+ value?: Option<T>[] | null;
78
+ onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
79
+ readOnly?: boolean;
80
+ disabled?: boolean;
81
+ className?: string;
82
+ isLoading?: boolean;
83
+ isNewItemDuplicate?: (existingItem: Option<T>, newItem: Option<T>) => boolean;
84
+ ref?: Ref<HTMLInputElement>;
85
+ };
86
+
87
+ /**
88
+ * @beta
89
+ * @since 2.44.0
90
+ */
91
+ const ChipInputField = function ChipInputField<T>(
92
+ {
93
+ label,
94
+ helpText,
95
+ readOnly,
96
+ disabled,
97
+ error,
98
+ createDeleteText,
99
+ onChange,
100
+ placeholder,
101
+ value,
102
+ className,
103
+ testId,
104
+ id,
105
+ children,
106
+ isLoading,
107
+ isNewItemDuplicate,
108
+ ...props
109
+ }: PropsWithRef<InputFieldProps<T>>,
110
+ ref: React.ForwardedRef<HTMLInputElement>
111
+ ) {
112
+ const [t] = useTranslation("commons", { keyPrefix: "form.chipList" });
113
+ const deleteTextCallback = useCallback(
114
+ (item) => (createDeleteText ? createDeleteText(item) : t("delete", { item })),
115
+ [createDeleteText, t]
116
+ );
117
+ const inputId = useAriaId(id ?? testId);
118
+ const labelId = useAriaId();
119
+ const inputDescriptionId = useAriaId();
120
+ const variant = error ? "danger" : undefined;
121
+ return (
122
+ <Field className={className} aria-owns={inputId}>
123
+ <Label id={labelId}>
124
+ {label}
125
+ {helpText ? <Help className="ml-1" text={helpText} /> : null}
126
+ </Label>
127
+ <div className={classNames("control", { "is-loading": isLoading })}>
128
+ <StyledChipInput
129
+ value={value}
130
+ onChange={(e) => onChange && onChange(e ?? [])}
131
+ className="is-flex is-flex-wrap-wrap input"
132
+ aria-labelledby={labelId}
133
+ disabled={readOnly || disabled}
134
+ isNewItemDuplicate={isNewItemDuplicate}
135
+ >
136
+ {value?.map((option, index) => (
137
+ <ChipInput.Chip key={option.label} className="tag is-light is-overflow-hidden">
138
+ <span className="is-ellipsis-overflow">{option.label}</span>
139
+ <StyledDelete aria-label={deleteTextCallback(option.label)} index={index} className="delete is-small" />
140
+ </ChipInput.Chip>
141
+ ))}
142
+ <StyledInput
143
+ {...props}
144
+ className={classNames(
145
+ "is-borderless",
146
+ "has-background-transparent",
147
+ "is-shadowless",
148
+ "input",
149
+ "is-ellipsis-overflow",
150
+ createVariantClass(variant)
151
+ )}
152
+ placeholder={!readOnly && !disabled ? placeholder : ""}
153
+ id={inputId}
154
+ ref={ref}
155
+ aria-describedby={inputDescriptionId}
156
+ {...createAttributesForTesting(testId)}
157
+ >
158
+ {children ? children : null}
159
+ </StyledInput>
160
+ </StyledChipInput>
161
+ </div>
162
+ <VisuallyHidden aria-hidden id={inputDescriptionId}>
163
+ {t("input.description")}
164
+ </VisuallyHidden>
165
+ {error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
166
+ </Field>
167
+ );
168
+ };
169
+ export default withForwardRef(ChipInputField);
@@ -0,0 +1,111 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import React, { ComponentProps } from "react";
26
+ import { Controller, ControllerRenderProps, Path } from "react-hook-form";
27
+ import { useScmFormContext } from "../ScmFormContext";
28
+ import { useScmFormPathContext } from "../FormPathContext";
29
+ import { defaultOptionFactory, prefixWithoutIndices, withForwardRef } from "../helpers";
30
+ import classNames from "classnames";
31
+ import ChipInputField from "./ChipInputField";
32
+ import { Option } from "@scm-manager/ui-types";
33
+
34
+ type Props<T extends Record<string, unknown>> = Omit<
35
+ Parameters<typeof ChipInputField>[0],
36
+ "error" | "createDeleteText" | "label" | "defaultChecked" | "required" | keyof ControllerRenderProps
37
+ > & {
38
+ rules?: ComponentProps<typeof Controller>["rules"];
39
+ name: Path<T>;
40
+ label?: string;
41
+ defaultValue?: string[];
42
+ createDeleteText?: (value: string) => string;
43
+ optionFactory?: (val: any) => Option<unknown>;
44
+ ref?: React.ForwardedRef<HTMLInputElement>;
45
+ };
46
+
47
+ /**
48
+ * @beta
49
+ * @since 2.44.0
50
+ */
51
+ function ControlledChipInputField<T extends Record<string, unknown>>(
52
+ {
53
+ name,
54
+ label,
55
+ helpText,
56
+ rules,
57
+ testId,
58
+ defaultValue,
59
+ readOnly,
60
+ placeholder,
61
+ className,
62
+ createDeleteText,
63
+ children,
64
+ optionFactory = defaultOptionFactory,
65
+ ...props
66
+ }: Props<T>,
67
+ ref: React.ForwardedRef<HTMLInputElement>
68
+ ) {
69
+ const { control, t, readOnly: formReadonly } = useScmFormContext();
70
+ const formPathPrefix = useScmFormPathContext();
71
+
72
+ const nameWithPrefix = formPathPrefix ? `${formPathPrefix}.${name}` : name;
73
+ const prefixedNameWithoutIndices = prefixWithoutIndices(nameWithPrefix);
74
+ const labelTranslation = label || t(`${prefixedNameWithoutIndices}.label`) || "";
75
+ const placeholderTranslation = placeholder || t(`${prefixedNameWithoutIndices}.placeholder`) || "";
76
+ const ariaLabelTranslation = t(`${prefixedNameWithoutIndices}.ariaLabel`);
77
+ const helpTextTranslation = helpText || t(`${prefixedNameWithoutIndices}.helpText`);
78
+ return (
79
+ <Controller
80
+ control={control}
81
+ name={nameWithPrefix}
82
+ rules={rules}
83
+ defaultValue={defaultValue}
84
+ render={({ field: { value, onChange, ...field }, fieldState }) => (
85
+ <ChipInputField
86
+ label={labelTranslation}
87
+ helpText={helpTextTranslation}
88
+ placeholder={placeholderTranslation}
89
+ aria-label={ariaLabelTranslation}
90
+ value={value ? value.map(optionFactory) : []}
91
+ onChange={(selectedOptions) => onChange(selectedOptions.map((item) => item.value))}
92
+ {...props}
93
+ {...field}
94
+ readOnly={readOnly ?? formReadonly}
95
+ className={classNames("column", className)}
96
+ error={
97
+ fieldState.error
98
+ ? fieldState.error.message || t(`${prefixedNameWithoutIndices}.error.${fieldState.error.type}`)
99
+ : undefined
100
+ }
101
+ testId={testId ?? `input-${nameWithPrefix}`}
102
+ ref={ref}
103
+ >
104
+ {children}
105
+ </ChipInputField>
106
+ )}
107
+ />
108
+ );
109
+ }
110
+
111
+ export default withForwardRef(ControlledChipInputField);
@@ -0,0 +1,125 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import { storiesOf } from "@storybook/react";
26
+ import React, { Fragment, useState } from "react";
27
+ import Combobox from "./Combobox";
28
+ import { Combobox as HeadlessCombobox } from "@headlessui/react";
29
+ import { Option } from "@scm-manager/ui-types";
30
+ import { Link, BrowserRouter } from "react-router-dom";
31
+
32
+ const waitFor = (ms: number) =>
33
+ function <T>(result: T) {
34
+ return new Promise<T>((resolve) => setTimeout(() => resolve(result), ms));
35
+ };
36
+
37
+ const data = [
38
+ { label: "Trillian", value: "1" },
39
+ { label: "Arthur", value: "2" },
40
+ { label: "Zaphod", value: "3" },
41
+ ];
42
+
43
+ const linkData = [{ label: "Link111111111111111111111111111111111111", value: "1" }];
44
+
45
+ storiesOf("Combobox", module)
46
+ .add("Options array", () => {
47
+ const [value, setValue] = useState<Option<string>>();
48
+ return <Combobox options={data} value={value} onChange={setValue} />;
49
+ })
50
+ .add("Options function", () => {
51
+ const [value, setValue] = useState<Option<string>>();
52
+ return <Combobox options={() => data} value={value} onChange={setValue} />;
53
+ })
54
+ .add("Options function as promise", () => {
55
+ const [value, setValue] = useState<Option<string>>();
56
+ return (
57
+ <Combobox
58
+ value={value}
59
+ onChange={setValue}
60
+ options={(query) => Promise.resolve(data.filter((t) => t.label.startsWith(query))).then(waitFor(1000))}
61
+ />
62
+ );
63
+ })
64
+ .add("Children as Element", () => {
65
+ const [value, setValue] = useState<Option<string>>();
66
+ const [query, setQuery] = useState("");
67
+
68
+ return (
69
+ <Combobox value={value} onChange={setValue} onQueryChange={setQuery}>
70
+ {query ? (
71
+ <HeadlessCombobox.Option value={{ label: query, value: query }} key={query} as={Fragment}>
72
+ {({ active }) => <Combobox.Option isActive={active}>{`Create ${query}`}</Combobox.Option>}
73
+ </HeadlessCombobox.Option>
74
+ ) : null}
75
+ <HeadlessCombobox.Option value={{ label: "All", value: "All" }} key="all" as={Fragment}>
76
+ {({ active }) => <Combobox.Option isActive={active}>All</Combobox.Option>}
77
+ </HeadlessCombobox.Option>
78
+ <>
79
+ {data.map((o) => (
80
+ <HeadlessCombobox.Option value={o} key={o.value} as={Fragment}>
81
+ {({ active }) => <Combobox.Option isActive={active}>{o.label}</Combobox.Option>}
82
+ </HeadlessCombobox.Option>
83
+ ))}
84
+ </>
85
+ </Combobox>
86
+ );
87
+ })
88
+ .add("Children as render props", () => {
89
+ const [value, setValue] = useState<Option<string>>();
90
+ return (
91
+ <Combobox options={data} value={value} onChange={setValue}>
92
+ {(o) => (
93
+ <HeadlessCombobox.Option value={o} key={o.value} as={Fragment}>
94
+ {({ active }) => <Combobox.Option isActive={active}>{o.label}</Combobox.Option>}
95
+ </HeadlessCombobox.Option>
96
+ )}
97
+ </Combobox>
98
+ );
99
+ })
100
+ .add("Links as render props", () => {
101
+ const [value, setValue] = useState<Option<string>>();
102
+ const [query, setQuery] = useState("Hello");
103
+ return (
104
+ <BrowserRouter>
105
+ <Combobox
106
+ className="input is-small omni-search-bar"
107
+ placeholder={"Placeholder"}
108
+ value={value}
109
+ options={linkData}
110
+ onChange={setValue}
111
+ onQueryChange={setQuery}
112
+ >
113
+ {(o) => (
114
+ <HeadlessCombobox.Option value={{ label: o.label, value: query, displayValue: o.value }} key={o.value} as={Fragment}>
115
+ {({ active }) => (
116
+ <Combobox.Option isActive={active}>
117
+ <Link to={o.label}>{o.label}</Link>
118
+ </Combobox.Option>
119
+ )}
120
+ </HeadlessCombobox.Option>
121
+ )}
122
+ </Combobox>
123
+ </BrowserRouter>
124
+ );
125
+ });