@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.
- package/.storybook/.babelrc +3 -0
- package/.storybook/RemoveThemesPlugin.js +57 -0
- package/.storybook/main.js +92 -0
- package/.storybook/preview-head.html +25 -0
- package/.storybook/preview.js +95 -0
- package/.storybook/withApiProvider.js +46 -0
- package/docs/introduction.stories.mdx +64 -0
- package/docs/usage.stories.mdx +22 -0
- package/package.json +81 -0
- package/src/base/buttons/Button.stories.tsx +89 -0
- package/src/base/buttons/Button.test.stories.mdx +74 -0
- package/src/base/buttons/Button.tsx +143 -0
- package/src/base/buttons/Icon.tsx +58 -0
- package/src/base/buttons/a11y.test.ts +34 -0
- package/src/base/buttons/docs/introduction.stories.mdx +64 -0
- package/src/base/buttons/docs/usage.stories.mdx +22 -0
- package/src/base/buttons/image-snapshot.test.ts +33 -0
- package/src/base/buttons/index.ts +26 -0
- package/src/base/forms/AddListEntryForm.tsx +127 -0
- package/src/base/forms/ConfigurationForm.tsx +59 -0
- package/src/base/forms/Form.stories.tsx +453 -0
- package/src/base/forms/Form.tsx +215 -0
- package/src/base/forms/FormPathContext.tsx +73 -0
- package/src/base/forms/FormRow.tsx +37 -0
- package/src/base/forms/ScmFormContext.tsx +43 -0
- package/src/base/forms/ScmFormListContext.tsx +65 -0
- package/src/base/forms/base/Control.tsx +34 -0
- package/src/base/forms/base/Field.tsx +35 -0
- package/src/base/forms/base/field-message/FieldMessage.tsx +34 -0
- package/src/base/forms/base/help/Help.tsx +34 -0
- package/src/base/forms/base/label/Label.tsx +35 -0
- package/src/base/forms/checkbox/Checkbox.stories.mdx +26 -0
- package/src/base/forms/checkbox/Checkbox.tsx +118 -0
- package/src/base/forms/checkbox/CheckboxField.tsx +39 -0
- package/src/base/forms/checkbox/ControlledCheckboxField.stories.mdx +36 -0
- package/src/base/forms/checkbox/ControlledCheckboxField.tsx +82 -0
- package/src/base/forms/chip-input/ChipInputField.stories.tsx +75 -0
- package/src/base/forms/chip-input/ChipInputField.tsx +169 -0
- package/src/base/forms/chip-input/ControlledChipInputField.tsx +111 -0
- package/src/base/forms/combobox/Combobox.stories.tsx +125 -0
- package/src/base/forms/combobox/Combobox.tsx +223 -0
- package/src/base/forms/combobox/ComboboxField.tsx +62 -0
- package/src/base/forms/combobox/ControlledComboboxField.tsx +96 -0
- package/src/base/forms/headless-chip-input/ChipInput.tsx +237 -0
- package/src/base/forms/helpers.ts +74 -0
- package/src/base/forms/index.ts +85 -0
- package/src/base/forms/input/ControlledInputField.stories.mdx +36 -0
- package/src/base/forms/input/ControlledInputField.tsx +87 -0
- package/src/base/forms/input/ControlledSecretConfirmationField.stories.mdx +39 -0
- package/src/base/forms/input/ControlledSecretConfirmationField.tsx +138 -0
- package/src/base/forms/input/Input.stories.mdx +22 -0
- package/src/base/forms/input/Input.tsx +46 -0
- package/src/base/forms/input/InputField.stories.mdx +22 -0
- package/src/base/forms/input/InputField.tsx +61 -0
- package/src/base/forms/input/Textarea.stories.mdx +28 -0
- package/src/base/forms/input/Textarea.tsx +46 -0
- package/src/base/forms/list/ControlledList.tsx +88 -0
- package/src/base/forms/radio-button/ControlledRadioGroupField.tsx +94 -0
- package/src/base/forms/radio-button/RadioButton.stories.tsx +226 -0
- package/src/base/forms/radio-button/RadioButton.tsx +116 -0
- package/src/base/forms/radio-button/RadioButtonContext.tsx +42 -0
- package/src/base/forms/radio-button/RadioGroup.tsx +49 -0
- package/src/base/forms/radio-button/RadioGroupField.tsx +58 -0
- package/src/base/forms/resourceHooks.ts +164 -0
- package/src/base/forms/select/ControlledSelectField.tsx +87 -0
- package/src/base/forms/select/Select.tsx +57 -0
- package/src/base/forms/select/SelectField.tsx +63 -0
- package/src/base/forms/table/ControlledColumn.tsx +49 -0
- package/src/base/forms/table/ControlledTable.tsx +99 -0
- package/src/base/forms/variants.ts +27 -0
- package/src/base/helpers/devbuild.ts +44 -0
- package/src/base/helpers/index.ts +26 -0
- package/src/base/helpers/useAriaId.tsx +31 -0
- package/src/base/index.ts +34 -0
- package/src/base/layout/_helpers/with-classes.tsx +52 -0
- package/src/base/layout/card/Card.stories.tsx +113 -0
- package/src/base/layout/card/Card.tsx +76 -0
- package/src/base/layout/card/CardDetail.tsx +196 -0
- package/src/base/layout/card/CardRow.tsx +46 -0
- package/src/base/layout/card/CardTitle.tsx +59 -0
- package/src/base/layout/card-list/CardList.stories.tsx +201 -0
- package/src/base/layout/card-list/CardList.tsx +76 -0
- package/src/base/layout/collapsible/Collapsible.stories.tsx +45 -0
- package/src/base/layout/collapsible/Collapsible.tsx +87 -0
- package/src/base/layout/index.ts +93 -0
- package/src/base/layout/tabs/TabTrigger.tsx +46 -0
- package/src/base/layout/tabs/Tabs.stories.tsx +48 -0
- package/src/base/layout/tabs/Tabs.tsx +52 -0
- package/src/base/layout/tabs/TabsContent.tsx +33 -0
- package/src/base/layout/tabs/TabsList.tsx +41 -0
- package/src/base/layout/templates/data-page/DataPage.stories.tsx +201 -0
- package/src/base/layout/templates/data-page/DataPageHeader.tsx +100 -0
- package/src/base/misc/Image.tsx +32 -0
- package/src/base/misc/Level.tsx +40 -0
- package/src/base/misc/Loading.tsx +64 -0
- package/src/base/misc/SubSubtitle.tsx +36 -0
- package/src/base/misc/Subtitle.tsx +37 -0
- package/src/base/misc/Title.tsx +56 -0
- package/src/base/misc/index.ts +30 -0
- package/src/base/notifications/BackendErrorNotification.tsx +160 -0
- package/src/base/notifications/ErrorNotification.tsx +73 -0
- package/src/base/notifications/Notification.tsx +48 -0
- package/src/base/notifications/index.tsx +27 -0
- package/src/base/overlays/dialog/Dialog.stories.tsx +64 -0
- package/src/base/overlays/dialog/Dialog.tsx +85 -0
- package/src/base/overlays/index.ts +44 -0
- package/src/base/overlays/menu/Menu.stories.tsx +78 -0
- package/src/base/overlays/menu/Menu.tsx +213 -0
- package/src/base/overlays/menu/MenuTrigger.tsx +63 -0
- package/src/base/overlays/popover/Popover.stories.tsx +69 -0
- package/src/base/overlays/popover/Popover.tsx +95 -0
- package/src/base/overlays/tooltip/Tooltip.examples.js +41 -0
- package/src/base/overlays/tooltip/Tooltip.stories.mdx +52 -0
- package/src/base/overlays/tooltip/Tooltip.tsx +96 -0
- package/src/base/shortcuts/index.ts +28 -0
- package/src/base/shortcuts/iterator/callbackIterator.ts +220 -0
- package/src/base/shortcuts/iterator/keyboardIterator.test.tsx +431 -0
- package/src/base/shortcuts/iterator/keyboardIterator.tsx +141 -0
- package/src/base/shortcuts/usePauseShortcuts.ts +44 -0
- package/src/base/shortcuts/useShortcut.ts +110 -0
- package/src/base/shortcuts/useShortcutDocs.tsx +54 -0
- package/src/base/text/SplitAndReplace.stories.tsx +83 -0
- package/src/base/text/SplitAndReplace.tsx +65 -0
- package/src/base/text/index.ts +25 -0
- package/src/base/text/textSplitAndReplace.test.ts +134 -0
- package/src/base/text/textSplitAndReplace.ts +86 -0
- package/src/index.ts +25 -0
- 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(" ") ? <> </> : "";
|
|
41
|
+
const last = s.endsWith(" ") ? <> </> : "";
|
|
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
|
+
});
|