@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,141 @@
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, { FC, useCallback, useContext, useEffect, useRef } from "react";
26
+ import { useTranslation } from "react-i18next";
27
+ import { useShortcut } from "../index";
28
+ import { Callback, CallbackIterator, CallbackRegistry, useCallbackIterator } from "./callbackIterator";
29
+
30
+ const KeyboardIteratorContext = React.createContext<CallbackRegistry>({
31
+ register: () => {
32
+ if (process.env.NODE_ENV === "development") {
33
+ // eslint-disable-next-line no-console
34
+ console.warn("Keyboard iterator targets have to be declared inside a KeyboardIterator");
35
+ }
36
+ return 0;
37
+ },
38
+ deregister: () => {
39
+ if (process.env.NODE_ENV === "development") {
40
+ // eslint-disable-next-line no-console
41
+ console.warn("Keyboard iterator targets have to be declared inside a KeyboardIterator");
42
+ }
43
+ },
44
+ });
45
+
46
+ export const useKeyboardIteratorItem = (item: Callback | CallbackIterator) => {
47
+ const { register, deregister } = useContext(KeyboardIteratorContext);
48
+ useEffect(() => {
49
+ const index = register(item);
50
+ return () => deregister(index);
51
+ }, [item, register, deregister]);
52
+ };
53
+
54
+ export const KeyboardSubIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => {
55
+ const callbackIterator = useCallbackIterator(initialIndex);
56
+
57
+ useKeyboardIteratorItem(callbackIterator);
58
+
59
+ return <KeyboardIteratorContext.Provider value={callbackIterator}>{children}</KeyboardIteratorContext.Provider>;
60
+ };
61
+
62
+ export const KeyboardIteratorContextProvider: FC<{ initialIndex?: number }> = ({ children, initialIndex }) => {
63
+ const [t] = useTranslation("commons");
64
+ const callbackIterator = useCallbackIterator(initialIndex);
65
+
66
+ useShortcut("k", callbackIterator.previous.bind(callbackIterator), {
67
+ description: t("shortcuts.iterator.previous"),
68
+ });
69
+
70
+ useShortcut("j", callbackIterator.next.bind(callbackIterator), {
71
+ description: t("shortcuts.iterator.next"),
72
+ });
73
+
74
+ useShortcut("tab", () => {
75
+ callbackIterator.reset();
76
+
77
+ return true;
78
+ });
79
+
80
+ return <KeyboardIteratorContext.Provider value={callbackIterator}>{children}</KeyboardIteratorContext.Provider>;
81
+ };
82
+
83
+ /**
84
+ * Use the {@link React.RefObject} returned from this hook to register a target to the nearest enclosing {@link KeyboardIterator} or {@link KeyboardSubIterator}.
85
+ *
86
+ * @example
87
+ * const ref = useKeyboardIteratorTarget();
88
+ * const target = <button ref={ref}>My Iteration Target</button>
89
+ */
90
+ export function useKeyboardIteratorTarget(): React.RefCallback<HTMLElement> {
91
+ const ref = useRef<HTMLElement>();
92
+ const callback = useCallback(() => ref.current?.focus(), []);
93
+ const refCallback: React.RefCallback<HTMLElement> = useCallback((el) => {
94
+ if (el) {
95
+ ref.current = el;
96
+ }
97
+ }, []);
98
+ useKeyboardIteratorItem(callback);
99
+ return refCallback;
100
+ }
101
+
102
+ /**
103
+ * Allows keyboard users to iterate through a list of items, defined by enclosed {@link useKeyboardIteratorTarget} invocations.
104
+ *
105
+ * The order is determined by the render order of the target hooks.
106
+ *
107
+ * Press `k` to navigate backwards and `j` to navigate forward.
108
+ * Pressing `tab` will reset the iterator to its initial state.
109
+ *
110
+ * Use the {@link KeyboardSubIterator} to wrap asynchronously loaded targets.
111
+ */
112
+ export const KeyboardIterator: FC = ({ children }) => (
113
+ <KeyboardIteratorContextProvider>{children}</KeyboardIteratorContextProvider>
114
+ );
115
+
116
+ /**
117
+ * Allows deferred {@link useKeyboardIteratorTarget} invocations enclosed in this sub-iterator to be registered in the correct order within a {@link KeyboardIterator}.
118
+ *
119
+ * This is especially useful for extension points which might contain further iterable elements that are loaded asynchronously.
120
+ *
121
+ * @example
122
+ * <KeyboardIterator>
123
+ * <KeyboardSubIterator>
124
+ * <ExtensionPoint<extensionPoints.RepositoryOverviewTop>
125
+ * name="repository.overview.top"
126
+ * renderAll={true}
127
+ * props={{
128
+ * page,
129
+ * search,
130
+ * namespace,
131
+ * }}
132
+ * />
133
+ * </KeyboardSubIterator>
134
+ * {groups.map((group) => {
135
+ * return <RepositoryGroupEntry group={group} key={group.name} />;
136
+ * })}
137
+ * </KeyboardIterator>
138
+ */
139
+ export const KeyboardSubIterator: FC = ({ children }) => (
140
+ <KeyboardSubIteratorContextProvider>{children}</KeyboardSubIteratorContextProvider>
141
+ );
@@ -0,0 +1,44 @@
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 { useEffect } from "react";
26
+ import Mousetrap from "mousetrap";
27
+ import "mousetrap/plugins/pause/mousetrap-pause.min.js";
28
+
29
+ /**
30
+ * Pauses or unpauses all shortcuts provided by {@link useShortcut}.
31
+ *
32
+ * @param pause Whether shortcuts should be paused
33
+ */
34
+ export default function usePauseShortcuts(pause: boolean) {
35
+ useEffect(() => {
36
+ if (pause) {
37
+ // @ts-ignore method comes from plugin
38
+ Mousetrap.pause();
39
+ } else {
40
+ // @ts-ignore method comes from plugin
41
+ Mousetrap.unpause();
42
+ }
43
+ }, [pause]);
44
+ }
@@ -0,0 +1,110 @@
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 { useEffect } from "react";
26
+ import Mousetrap from "mousetrap";
27
+ import useShortcutDocs from "./useShortcutDocs";
28
+
29
+ export type UseShortcutOptions = {
30
+ /**
31
+ * Whether the shortcut is currently active
32
+ *
33
+ * @default true
34
+ */
35
+ active?: boolean;
36
+
37
+ /**
38
+ * The translated description used for the shortcut documentation.
39
+ *
40
+ * If no description is supplied, there will be no entry in the shortcut summary table.
41
+ */
42
+ description?: string | null;
43
+ };
44
+
45
+ /**
46
+ * ## Summary
47
+ *
48
+ * Binds a global keyboard shortcut to a given callback.
49
+ *
50
+ * The callback is automatically cleaned up upon unmount.
51
+ *
52
+ * ## Supported keys
53
+ *
54
+ * For modifier keys you can use shift, ctrl, alt, or meta.
55
+ *
56
+ * You can substitute option for alt and command for meta.
57
+ *
58
+ * Other special keys are backspace, tab, enter, return, capslock, esc, escape, space, pageup, pagedown, end, home, left, up, right, down, ins, del, and plus.
59
+ *
60
+ * Any other key you should be able to reference by name like a, /, $, *, or =.
61
+ *
62
+ * A "mod" helper exists which maps to either "command" on Mac and "ctrl" on Windows and Linux.
63
+ *
64
+ * ## Combinations
65
+ *
66
+ * Keys can be combined by separating them with a whitespace.
67
+ * For using modifiers, prefix the key with the modifier and concat them with a "+".
68
+ *
69
+ * Please also refer to the examples.
70
+ *
71
+ * @param key The keycode combination that triggers the callback
72
+ * @param callback The function that is executed when the key combination is pressed, returning `true` additionally executes default browser behaviour
73
+ * @param options Whether the shortcut is currently active, defaults to true
74
+ * @example useShortcut("a b", ...)
75
+ * @example useShortcut("ctrl+shift+k", ...)
76
+ * @see https://github.com/ccampbell/mousetrap
77
+ * @see https://craig.is/killing/mice
78
+ */
79
+ export default function useShortcut(
80
+ key: string,
81
+ callback: (e: KeyboardEvent) => void | boolean,
82
+ options?: UseShortcutOptions
83
+ ) {
84
+ const { add, remove } = useShortcutDocs();
85
+ useEffect(() => {
86
+ const active = options?.active ?? true;
87
+ const description = options?.description;
88
+ if (active) {
89
+ if (description) {
90
+ add(key, description);
91
+ }
92
+ Mousetrap.bind(key, (e) => {
93
+ const callbackResult = callback(e);
94
+ /*
95
+ * Returning false by default disables standard browser event behaviour and stops event bubbling.
96
+ * Otherwise, a shortcut that moves focus to an input field would cause the key to be entered into the input at the same time.
97
+ * Shortcuts can explicitly return `true` to re-enable event bubbling and browser behaviour.
98
+ */
99
+ return callbackResult ?? false;
100
+ });
101
+ }
102
+
103
+ return () => {
104
+ if (description) {
105
+ remove(key);
106
+ }
107
+ Mousetrap.unbind(key);
108
+ };
109
+ }, [key, callback, add, remove, options]);
110
+ }
@@ -0,0 +1,54 @@
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 type { FC } from "react";
26
+ import React, { useContext, useMemo, useRef } from "react";
27
+
28
+ export type ShortcutDocsContextType = {
29
+ docs: Readonly<Record<string, string>>;
30
+ add: (key: string, description: string) => void;
31
+ remove: (key: string) => void;
32
+ };
33
+
34
+ const ShortcutDocsContext = React.createContext<ShortcutDocsContextType>({} as ShortcutDocsContextType);
35
+
36
+ export const ShortcutDocsContextProvider: FC = ({ children }) => {
37
+ const docs = useRef<Record<string, string>>({});
38
+ const value = useMemo(
39
+ () => ({
40
+ docs: docs.current,
41
+ add: (key: string, description: string) => (docs.current[key] = description),
42
+ remove: (key: string) => {
43
+ delete docs.current[key];
44
+ },
45
+ }),
46
+ []
47
+ );
48
+
49
+ return <ShortcutDocsContext.Provider value={value}>{children}</ShortcutDocsContext.Provider>;
50
+ };
51
+
52
+ export default function useShortcutDocs() {
53
+ return useContext(ShortcutDocsContext);
54
+ }
@@ -0,0 +1,83 @@
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 from "react";
26
+ import { ComponentMeta, ComponentStory } from "@storybook/react";
27
+ import SplitAndReplace from "./SplitAndReplace";
28
+
29
+ export default {
30
+ title: "SplitAndReplace",
31
+ component: SplitAndReplace,
32
+ } as ComponentMeta<typeof SplitAndReplace>;
33
+
34
+ const textOne = "'So this is it,` said Arthur, 'We are going to die.`";
35
+ const textTwo = "'Yes,` said Ford, 'except... no! Wait a minute!`";
36
+
37
+ export const ReplaceOnce: ComponentStory<typeof SplitAndReplace> = () => {
38
+ const replacements = [
39
+ {
40
+ textToReplace: "'",
41
+ replacement: <span>!</span>,
42
+ replaceAll: false,
43
+ },
44
+ ];
45
+ return (
46
+ <div>
47
+ <h3>Replace ' with ! once</h3>
48
+ <label>Original text:</label>
49
+ <p>{textOne}</p>
50
+ <label>Text with replacements:</label>
51
+ <p>
52
+ <SplitAndReplace text={textOne} replacements={replacements} />
53
+ </p>
54
+ </div>
55
+ );
56
+ };
57
+
58
+ export const ReplaceAll: ComponentStory<typeof SplitAndReplace> = () => {
59
+ const replacements = [
60
+ {
61
+ textToReplace: "'",
62
+ replacement: <span>!</span>,
63
+ replaceAll: true,
64
+ },
65
+ {
66
+ textToReplace: "`",
67
+ replacement: <span>?</span>,
68
+ replaceAll: true,
69
+ },
70
+ ];
71
+ return (
72
+ <div>
73
+ <h3>Replace all ` with ? and ' with !</h3>
74
+ <label>Original text:</label>
75
+ <p>{textTwo}</p>
76
+
77
+ <label> Text with replacements: </label>
78
+ <p>
79
+ <SplitAndReplace text={textTwo} replacements={replacements} />
80
+ </p>
81
+ </div>
82
+ );
83
+ };
@@ -0,0 +1,65 @@
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
+ import React, { FC, ReactNode } from "react";
25
+ import textSplitAndReplace from "./textSplitAndReplace";
26
+
27
+ export type Replacement = {
28
+ textToReplace: string;
29
+ replacement: ReactNode;
30
+ replaceAll?: boolean;
31
+ };
32
+
33
+ export type Props = {
34
+ text: string;
35
+ replacements: Replacement[];
36
+ textWrapper?: (s: string) => ReactNode;
37
+ };
38
+
39
+ const defaultTextWrapper = (s: string) => {
40
+ const first = s.startsWith(" ") ? <>&nbsp;</> : "";
41
+ const last = s.endsWith(" ") ? <>&nbsp;</> : "";
42
+ return (
43
+ <>
44
+ {first}
45
+ {s}
46
+ {last}
47
+ </>
48
+ );
49
+ };
50
+
51
+ const SplitAndReplace: FC<Props> = ({ text, replacements, textWrapper = defaultTextWrapper }) => {
52
+ const parts = textSplitAndReplace<ReactNode>(text, replacements, textWrapper);
53
+ if (parts.length === 0) {
54
+ return <>{parts[0]}</>;
55
+ }
56
+ return (
57
+ <>
58
+ {parts.map((part, index) => (
59
+ <React.Fragment key={index}>{part}</React.Fragment>
60
+ ))}
61
+ </>
62
+ );
63
+ };
64
+
65
+ export default SplitAndReplace;
@@ -0,0 +1,25 @@
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
+ export { default as SplitAndReplace, Replacement } from "./SplitAndReplace";
@@ -0,0 +1,134 @@
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 textSplitAndReplace from "./textSplitAndReplace";
26
+
27
+ type Wrapped = {
28
+ text: string;
29
+ };
30
+
31
+ const testWrapper = (s: string) => {
32
+ return { text: s };
33
+ };
34
+
35
+ describe("text split and replace", () => {
36
+ it("should wrap text if nothing should be replaced", () => {
37
+ const result = textSplitAndReplace<Wrapped>("Don't Panic.", [], testWrapper);
38
+ expect(result).toHaveLength(1);
39
+ expect(result[0]).toStrictEqual({ text: "Don't Panic." });
40
+ });
41
+
42
+ it("should replace single string", () => {
43
+ const result = textSplitAndReplace<Wrapped>(
44
+ "Don't Panic.",
45
+ [{ textToReplace: "'", replacement: { text: "`" } }],
46
+ testWrapper
47
+ );
48
+ expect(result).toHaveLength(3);
49
+ expect(result[0]).toStrictEqual({ text: "Don" });
50
+ expect(result[1]).toStrictEqual({ text: "`" });
51
+ expect(result[2]).toStrictEqual({ text: "t Panic." });
52
+ });
53
+
54
+ it("should replace strings only once if replace all is not set", () => {
55
+ const result = textSplitAndReplace<Wrapped>(
56
+ "'So this is it,' said Arthur, 'We are going to die.'",
57
+ [{ textToReplace: "'", replacement: { text: "“" } }],
58
+ testWrapper
59
+ );
60
+ expect(result).toHaveLength(2);
61
+ expect(result[0]).toStrictEqual({ text: "“" });
62
+ expect(result[1]).toStrictEqual({ text: "So this is it,' said Arthur, 'We are going to die.'" });
63
+ });
64
+
65
+ it("should replace all strings if replace all is set to true", () => {
66
+ const result = textSplitAndReplace<Wrapped>(
67
+ "'So this is it,' said Arthur, 'We are going to die.'",
68
+ [{ textToReplace: "'", replacement: { text: "“" }, replaceAll: true }],
69
+ testWrapper
70
+ );
71
+ expect(result).toHaveLength(7);
72
+ expect(result[0]).toStrictEqual({ text: "“" });
73
+ expect(result[1]).toStrictEqual({ text: "So this is it," });
74
+ expect(result[2]).toStrictEqual({ text: "“" });
75
+ expect(result[3]).toStrictEqual({ text: " said Arthur, " });
76
+ expect(result[4]).toStrictEqual({ text: "“" });
77
+ expect(result[5]).toStrictEqual({ text: "We are going to die." });
78
+ expect(result[6]).toStrictEqual({ text: "“" });
79
+ });
80
+
81
+ it("should replace strings with multiple replacements", () => {
82
+ const result = textSplitAndReplace<Wrapped>(
83
+ "'So this is it,' said Arthur, 'We are going to die.'",
84
+ [
85
+ { textToReplace: "'", replacement: { text: "“" }, replaceAll: true },
86
+ { textToReplace: "Arthur", replacement: { text: "Dent" }, replaceAll: true },
87
+ ],
88
+ testWrapper
89
+ );
90
+ expect(result).toHaveLength(9);
91
+ expect(result[0]).toStrictEqual({ text: "“" });
92
+ expect(result[1]).toStrictEqual({ text: "So this is it," });
93
+ expect(result[2]).toStrictEqual({ text: "“" });
94
+ expect(result[3]).toStrictEqual({ text: " said " });
95
+ expect(result[4]).toStrictEqual({ text: "Dent" });
96
+ expect(result[5]).toStrictEqual({ text: ", " });
97
+ expect(result[6]).toStrictEqual({ text: "“" });
98
+ expect(result[7]).toStrictEqual({ text: "We are going to die." });
99
+ expect(result[8]).toStrictEqual({ text: "“" });
100
+ });
101
+
102
+ it("should ignore conflicting replacements", () => {
103
+ const result = textSplitAndReplace<Wrapped>(
104
+ "'So this is it,' said Arthur, 'We are going to die.'",
105
+ [
106
+ { textToReplace: "said Arthur", replacement: { text: "to be replaced" } },
107
+ { textToReplace: " said", replacement: { text: "to be ignored 1" }, replaceAll: true },
108
+ { textToReplace: "d A", replacement: { text: "to be ignored 2" }, replaceAll: true },
109
+ { textToReplace: "Arthur,", replacement: { text: "to be ignored 3" }, replaceAll: true },
110
+ ],
111
+ testWrapper
112
+ );
113
+ expect(result).toHaveLength(3);
114
+ expect(result[0]).toStrictEqual({ text: "'So this is it,' " });
115
+ expect(result[1]).toStrictEqual({ text: "to be replaced" });
116
+ expect(result[2]).toStrictEqual({ text: ", 'We are going to die.'" });
117
+ });
118
+
119
+ it("should replace adjacent texts", () => {
120
+ const result = textSplitAndReplace<Wrapped>(
121
+ "'So this is it,' said Arthur, 'We are going to die.'",
122
+ [
123
+ { textToReplace: "'So this is it,'", replacement: { text: "one" } },
124
+ { textToReplace: " said Arthur, ", replacement: { text: "two" } },
125
+ { textToReplace: "'We are going to die.'", replacement: { text: "three" } },
126
+ ],
127
+ testWrapper
128
+ );
129
+ expect(result).toHaveLength(3);
130
+ expect(result[0]).toStrictEqual({ text: "one" });
131
+ expect(result[1]).toStrictEqual({ text: "two" });
132
+ expect(result[2]).toStrictEqual({ text: "three" });
133
+ });
134
+ });