@eccenca/gui-elements 23.7.0-rc.1 → 23.7.0-rc.3

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 (56) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/cjs/cmem/markdown/Markdown.js +1 -1
  3. package/dist/cjs/cmem/markdown/Markdown.js.map +1 -1
  4. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js +3 -3
  5. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  6. package/dist/cjs/components/Breadcrumb/BreadcrumbList.js +1 -1
  7. package/dist/cjs/components/Breadcrumb/BreadcrumbList.js.map +1 -1
  8. package/dist/cjs/components/Card/Card.js +3 -1
  9. package/dist/cjs/components/Card/Card.js.map +1 -1
  10. package/dist/cjs/components/MultiSelect/MultiSelect.js +8 -34
  11. package/dist/cjs/components/MultiSelect/MultiSelect.js.map +1 -1
  12. package/dist/cjs/components/Sticky/StickyTarget.js +46 -5
  13. package/dist/cjs/components/Sticky/StickyTarget.js.map +1 -1
  14. package/dist/cjs/components/TextField/TextArea.js +85 -8
  15. package/dist/cjs/components/TextField/TextArea.js.map +1 -1
  16. package/dist/esm/cmem/markdown/Markdown.js +1 -1
  17. package/dist/esm/cmem/markdown/Markdown.js.map +1 -1
  18. package/dist/esm/components/AutocompleteField/AutoCompleteField.js +3 -3
  19. package/dist/esm/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  20. package/dist/esm/components/Breadcrumb/BreadcrumbList.js +1 -1
  21. package/dist/esm/components/Breadcrumb/BreadcrumbList.js.map +1 -1
  22. package/dist/esm/components/Card/Card.js +4 -2
  23. package/dist/esm/components/Card/Card.js.map +1 -1
  24. package/dist/esm/components/MultiSelect/MultiSelect.js +8 -34
  25. package/dist/esm/components/MultiSelect/MultiSelect.js.map +1 -1
  26. package/dist/esm/components/Sticky/StickyTarget.js +46 -5
  27. package/dist/esm/components/Sticky/StickyTarget.js.map +1 -1
  28. package/dist/esm/components/TextField/TextArea.js +86 -9
  29. package/dist/esm/components/TextField/TextArea.js.map +1 -1
  30. package/dist/types/cmem/ActivityControl/ActivityControlTypes.d.ts +1 -0
  31. package/dist/types/components/AutocompleteField/AutoCompleteField.d.ts +1 -1
  32. package/dist/types/components/Breadcrumb/BreadcrumbList.d.ts +2 -1
  33. package/dist/types/components/Card/Card.d.ts +8 -2
  34. package/dist/types/components/MultiSelect/MultiSelect.d.ts +14 -9
  35. package/dist/types/components/Sticky/StickyTarget.d.ts +14 -3
  36. package/dist/types/components/TextField/TextArea.d.ts +28 -3
  37. package/package.json +1 -1
  38. package/src/cmem/ActivityControl/ActivityControlTypes.ts +2 -0
  39. package/src/cmem/markdown/Markdown.tsx +1 -1
  40. package/src/components/Application/application.scss +0 -1
  41. package/src/components/AutocompleteField/AutoCompleteField.tsx +4 -4
  42. package/src/components/Breadcrumb/BreadcrumbList.tsx +3 -3
  43. package/src/components/Card/Card.tsx +15 -3
  44. package/src/components/Card/card.scss +6 -1
  45. package/src/components/Icon/stories/Icon.stories.tsx +1 -1
  46. package/src/components/MultiSelect/MultiSelect.tsx +30 -43
  47. package/src/components/MultiSuggestField/MultiSuggestField.stories.tsx +1 -2
  48. package/src/components/MultiSuggestField/tests/MultiSuggestField.test.tsx +90 -6
  49. package/src/components/Sticky/StickyTarget.tsx +63 -7
  50. package/src/components/Sticky/sticky.scss +71 -12
  51. package/src/components/TextField/TextArea.tsx +174 -12
  52. package/src/components/TextField/stories/TextArea.stories.tsx +39 -12
  53. package/src/components/TextField/textfield.scss +81 -11
  54. package/src/includes/blueprintjs/_requisits.scss +1 -1
  55. package/src/includes/blueprintjs/_variables.scss +3 -172
  56. package/src/includes/carbon-components/_requisits.scss +1 -0
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { CardProps as BlueprintCardProps } from "@blueprintjs/core";
3
- export interface CardProps extends BlueprintCardProps {
2
+ import { CardProps as BlueprintCardProps, Elevation as BlueprintCardElevation } from "@blueprintjs/core";
3
+ export interface CardProps extends Omit<BlueprintCardProps, "elevation"> {
4
4
  /**
5
5
  * `<Card />` element is included in DOM as simple `div` element.
6
6
  * By default it is a HTML `section`.
@@ -14,6 +14,12 @@ export interface CardProps extends BlueprintCardProps {
14
14
  * Background color is slightly altered to differ card display from other cards.
15
15
  */
16
16
  elevated?: boolean;
17
+ /**
18
+ * Controls the intensity of the drop shadow beneath the card.
19
+ * At elevation `0`, no drop shadow is applied.
20
+ * At elevation `-1`, the card is even borderless.
21
+ */
22
+ elevation?: -1 | BlueprintCardElevation;
17
23
  /**
18
24
  * When card (or its children) get focus the card is scrolled into the viewport.
19
25
  * Property value defined which part of the card is always scrolled in, this may important when the card is larger than the viewport.
@@ -9,11 +9,7 @@ export interface MultiSelectSelectionProps<T> {
9
9
  createdItems: Partial<T>[];
10
10
  }
11
11
  export declare type SelectedParamsType<T> = MultiSelectSelectionProps<T>;
12
- export interface MultiSelectProps<T> extends TestableComponent, Pick<BlueprintMultiSelectProps<T>, "items" | "placeholder" | "openOnKeyDown"> {
13
- /**
14
- * Predefined selected values
15
- */
16
- selectedItems?: T[];
12
+ interface MultiSelectCommonProps<T> extends TestableComponent, Pick<BlueprintMultiSelectProps<T>, "items" | "placeholder" | "openOnKeyDown"> {
17
13
  /**
18
14
  * Additional class name, space separated.
19
15
  */
@@ -27,10 +23,6 @@ export interface MultiSelectProps<T> extends TestableComponent, Pick<BlueprintMu
27
23
  * this would be used in the item selection list as well as the multi-select input
28
24
  */
29
25
  itemLabel: (item: T) => string;
30
- /**
31
- * When set to true will set the multi-select value with all the items provided
32
- */
33
- prePopulateWithItems?: boolean;
34
26
  /**
35
27
  * function handler that would be called anytime an item is selected/deselected or an item is created/removed
36
28
  */
@@ -107,6 +99,19 @@ export interface MultiSelectProps<T> extends TestableComponent, Pick<BlueprintMu
107
99
  */
108
100
  wrapperProps?: React.HTMLAttributes<HTMLDivElement>;
109
101
  }
102
+ export declare type MultiSelectProps<T> = MultiSelectCommonProps<T> & ({
103
+ /**
104
+ * Predefined selected values
105
+ */
106
+ selectedItems?: T[];
107
+ prePopulateWithItems?: never;
108
+ } | {
109
+ selectedItems?: never;
110
+ /**
111
+ * When set to true will set the multi-select value with all the items provided
112
+ */
113
+ prePopulateWithItems?: boolean;
114
+ });
110
115
  /**
111
116
  * **Element is deprecated for the current type of usage.**
112
117
  * Use `MultiSuggestField` as replacement.
@@ -9,15 +9,26 @@ export interface StickyTargetProps extends React.HTMLAttributes<HTMLDivElement>
9
9
  * The application header is not taken into offset calculation
10
10
  */
11
11
  local?: boolean;
12
+ /**
13
+ * Set additional distance to original sticky position.
14
+ */
15
+ offset?: `${number}${string}`;
12
16
  /**
13
17
  * Set the background color used for the sticky area.
14
18
  * As it can overlay other content readability could be harmed if the overlayed content is shining through.
15
19
  */
16
20
  background?: "card" | "application" | "transparent";
17
21
  /**
18
- * Set additional distance to original sticky position.
22
+ * In some situations there could be a gap between sticky target area and the border of the related scroll area.
23
+ * The main gap is the gap towards the direction of the sticky behaviour, specified by `to`.
24
+ * You can fill this gap with a gradient or full background color.
19
25
  */
20
- offset?: `${number}${string}`;
26
+ fillMainGap?: "full" | "gradient";
27
+ /**
28
+ * The secondary gap is the gap against the direction of the sticky behaviour.
29
+ * So in case of `to="top"` this is rendered on the bottom of the sticky area.
30
+ */
31
+ fillSecondaryGap?: "full" | "gradient";
21
32
  /**
22
33
  * Callback that returns an DOM element.
23
34
  * The position of `StickyTarget` is then calculated relative to that element.
@@ -28,5 +39,5 @@ export interface StickyTargetProps extends React.HTMLAttributes<HTMLDivElement>
28
39
  * Element wraps the content that need to be displayed sticky.
29
40
  * The content then offset relative to its nearest scrolling ancestor and containing block (nearest block-level ancestor).
30
41
  */
31
- export declare const StickyTarget: ({ className, to, local, background, offset, style, getConnectedElement, ...otherDivProps }: StickyTargetProps) => React.JSX.Element;
42
+ export declare const StickyTarget: ({ className, to, local, offset, background, fillMainGap, fillSecondaryGap, style, getConnectedElement, ...otherDivProps }: StickyTargetProps) => React.JSX.Element;
32
43
  export default StickyTarget;
@@ -1,27 +1,52 @@
1
1
  import React from "react";
2
- import { TextAreaProps as BlueprintTextAreaProps } from "@blueprintjs/core";
2
+ import { MaybeElement, TextAreaProps as BlueprintTextAreaProps } from "@blueprintjs/core";
3
+ import { IntentTypes } from "../../common/Intent";
4
+ import { ValidIconName } from "../Icon/canonicalIconNames";
3
5
  import { InvisibleCharacterWarningProps } from "./useTextValidation";
4
- export interface TextAreaProps extends Partial<BlueprintTextAreaProps> {
6
+ export interface TextAreaProps extends Omit<BlueprintTextAreaProps, "intent"> {
5
7
  /**
6
8
  * when set to true the input takes a blue border color
9
+ * @deprecated Use the `intent` property.
7
10
  */
8
11
  hasStatePrimary?: boolean;
9
12
  /**
10
13
  * when set to true the input takes a green border color
14
+ * @deprecated Use the `intent` property.
11
15
  */
12
16
  hasStateSuccess?: boolean;
13
17
  /**
14
18
  * when set to true the input takes an orange border color
19
+ * @deprecated Use the `intent` property.
15
20
  */
16
21
  hasStateWarning?: boolean;
17
22
  /**
18
23
  * when set to true the input takes a red border color
24
+ * @deprecated Use the `intent` property.
19
25
  */
20
26
  hasStateDanger?: boolean;
27
+ /**
28
+ * Intent state of the text area.
29
+ */
30
+ intent?: IntentTypes | "edited" | "removed";
21
31
  /**
22
32
  * If set, allows to be informed of invisible, hard to spot characters in the string value.
23
33
  */
24
34
  invisibleCharacterWarning?: InvisibleCharacterWarningProps;
35
+ /**
36
+ * Left aligned icon, can be a canonical icon name or an `Icon` element.
37
+ * This will update left padding on the text area.
38
+ */
39
+ leftIcon?: ValidIconName | MaybeElement;
40
+ /**
41
+ * Element to render on right side of text area. Should be not too large.
42
+ * This will update right padding on the text area.
43
+ */
44
+ rightElement?: JSX.Element;
45
+ /**
46
+ * Add HTML properties to the wrapper element.
47
+ * The element wraps `TextArea` in case of a given `wrapperDivProps`, `leftIcon` or `rightElement` property.
48
+ */
49
+ wrapperDivProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "children">;
25
50
  }
26
- export declare const TextArea: ({ className, hasStatePrimary, hasStateSuccess, hasStateWarning, hasStateDanger, rows, invisibleCharacterWarning, ...otherProps }: TextAreaProps) => React.JSX.Element;
51
+ export declare const TextArea: ({ className, hasStatePrimary, hasStateSuccess, hasStateWarning, hasStateDanger, rows, invisibleCharacterWarning, leftIcon, rightElement, wrapperDivProps, ...otherProps }: TextAreaProps) => React.JSX.Element;
27
52
  export default TextArea;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eccenca/gui-elements",
3
3
  "description": "GUI elements based on other libraries, usable in React application, written in Typescript.",
4
- "version": "23.7.0-rc.1",
4
+ "version": "23.7.0-rc.3",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/eccenca/gui-elements",
7
7
  "bugs": "https://github.com/eccenca/gui-elements/issues",
@@ -16,6 +16,8 @@ export interface IActivityStatus {
16
16
  statusName: "Waiting" | "Finished" | "Idle" | "Running" | "Canceling";
17
17
  // A number between 0 and 100
18
18
  progress: number;
19
+ // timestamp for last update
20
+ lastUpdateTime:string;
19
21
  // More information corresponding to the status
20
22
  message: string;
21
23
  // If the activity has been cancelled
@@ -3,7 +3,7 @@ import ReactMarkdown from "react-markdown";
3
3
  import { PluggableList } from "react-markdown/lib/react-markdown";
4
4
  import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
5
5
  import { materialLight } from "react-syntax-highlighter/dist/esm/styles/prism";
6
- // @ts-expect-error: No declaration file for module
6
+ // @ts-ignore: No declaration file for module (TODO: should be @ts-expect-error but GUI elements is used inside project with `noImplicitAny=false`)
7
7
  import remarkTypograf from "@mavrin/remark-typograf";
8
8
  import rehypeRaw from "rehype-raw";
9
9
  import { remarkDefinitionList } from "remark-definition-list";
@@ -1,5 +1,4 @@
1
1
  // @import 'config';
2
- @import "~@carbon/styles/scss/components/ui-shell/functions";
3
2
  @import "header";
4
3
  @import "toolbar";
5
4
 
@@ -157,7 +157,7 @@ export interface AutoCompleteFieldProps<T, UPDATE_VALUE> {
157
157
  */
158
158
  fill?: boolean;
159
159
  /** Utility that fetches more options when clicked*/
160
- loadMoreResults?: () => Promise<T[]>;
160
+ loadMoreResults?: () => Promise<T[] | undefined>;
161
161
  }
162
162
 
163
163
  export type IAutoCompleteFieldProps<T, UPDATE_VALUE> = AutoCompleteFieldProps<T, UPDATE_VALUE>;
@@ -452,13 +452,13 @@ export function AutoCompleteField<T, UPDATE_VALUE>(props: AutoCompleteFieldProps
452
452
  const menu = event.target;
453
453
  const { scrollTop, scrollHeight, clientHeight } = menu;
454
454
  // Check if scrolled to the bottom of the list
455
- if (scrollTop + clientHeight >= scrollHeight && loadMoreResults) {
455
+ if (Math.round(scrollTop + clientHeight) >= scrollHeight && loadMoreResults) {
456
456
  const results = await loadMoreResults();
457
457
  if (results) {
458
458
  setFiltered((prev) => [...prev, ...results]);
459
459
  setTimeout(() => {
460
- menu.scrollTop = scrollHeight; //safari adaptation
461
- menu.scrollTo({ left: 0, top: scrollHeight, behavior: "auto" });
460
+ menu.scrollTop = scrollTop; //safari adaptation
461
+ menu.scrollTo({ left: 0, top: scrollTop, behavior: "auto" });
462
462
  });
463
463
  }
464
464
  }
@@ -10,7 +10,6 @@ import { TestableComponent } from "../interfaces";
10
10
  import BreadcrumbItem from "./BreadcrumbItem";
11
11
  import { BreadcrumbItemProps } from "./BreadcrumbItem";
12
12
 
13
- // FIXME: enforce onItemClick later when href value can always be routed correctly
14
13
  export interface BreadcrumbListProps
15
14
  extends TestableComponent,
16
15
  Omit<
@@ -28,7 +27,8 @@ export interface BreadcrumbListProps
28
27
  */
29
28
  items: BreadcrumbItemProps[];
30
29
  /**
31
- click handler used on breadcrumb items
30
+ Click handler used on all breadcrumb items using their `href` property.
31
+ Is only used if the breadcrumb item have not defined an own `onClick` handler.
32
32
  */
33
33
  onItemClick?(itemUrl: string | undefined, event: object): boolean | void;
34
34
  /**
@@ -80,7 +80,7 @@ export const BreadcrumbList = ({
80
80
  /*itemDivider="/"*/
81
81
  {...otherProps}
82
82
  onClick={
83
- onItemClick
83
+ onItemClick && propsBreadcrumb.href && !onClick
84
84
  ? (e) => {
85
85
  onItemClick(propsBreadcrumb.href, e);
86
86
  }
@@ -1,9 +1,13 @@
1
1
  import React from "react";
2
- import { Card as BlueprintCard, CardProps as BlueprintCardProps } from "@blueprintjs/core";
2
+ import {
3
+ Card as BlueprintCard,
4
+ CardProps as BlueprintCardProps,
5
+ Elevation as BlueprintCardElevation,
6
+ } from "@blueprintjs/core";
3
7
 
4
8
  import { CLASSPREFIX as eccgui } from "../../configuration/constants";
5
9
 
6
- export interface CardProps extends BlueprintCardProps {
10
+ export interface CardProps extends Omit<BlueprintCardProps, "elevation"> {
7
11
  /**
8
12
  * `<Card />` element is included in DOM as simple `div` element.
9
13
  * By default it is a HTML `section`.
@@ -17,6 +21,12 @@ export interface CardProps extends BlueprintCardProps {
17
21
  * Background color is slightly altered to differ card display from other cards.
18
22
  */
19
23
  elevated?: boolean;
24
+ /**
25
+ * Controls the intensity of the drop shadow beneath the card.
26
+ * At elevation `0`, no drop shadow is applied.
27
+ * At elevation `-1`, the card is even borderless.
28
+ */
29
+ elevation?: -1 | BlueprintCardElevation;
20
30
  /**
21
31
  * When card (or its children) get focus the card is scrolled into the viewport.
22
32
  * Property value defined which part of the card is always scrolled in, this may important when the card is larger than the viewport.
@@ -48,6 +58,7 @@ export const Card = ({
48
58
  ? {
49
59
  tabIndex: 0,
50
60
  onFocus: (e: any) => {
61
+ // FIXME: we should not have any hard relations to apps that using this lib
51
62
  const el = e.target.closest(".diapp-iframewindow__content");
52
63
  setTimeout(() => {
53
64
  if (el)
@@ -68,9 +79,10 @@ export const Card = ({
68
79
  (elevated ? ` ${eccgui}-card--elevated` : "") +
69
80
  (scrollinOnFocus ? ` ${eccgui}-card--scrollonfocus` : "") +
70
81
  (whitespaceAmount !== "medium" ? ` ${eccgui}-card--whitespace-${whitespaceAmount}` : "") +
82
+ (elevation < 0 ? ` ${eccgui}-card--whitespace-borderless` : "") +
71
83
  (className ? ` ${className}` : "")
72
84
  }
73
- elevation={elevation}
85
+ elevation={Math.max(0, elevation) as BlueprintCardElevation}
74
86
  interactive={otherProps.onClick ? true : interactive}
75
87
  {...scrollIn}
76
88
  {...otherProps}
@@ -1,9 +1,10 @@
1
+ @use "sass:color";
2
+
1
3
  $card-padding: 0 !default;
2
4
  $card-background-color: $white !default;
3
5
  $card-selected-background-color: rgba($blue3, 0.1);
4
6
  $eccgui-size-card-spacing: $eccgui-size-typo-base !default;
5
7
 
6
- @use "sass:color";
7
8
  @import "~@blueprintjs/core/src/components/card/card";
8
9
 
9
10
  .#{$eccgui}-card {
@@ -61,6 +62,10 @@ $eccgui-size-card-spacing: $eccgui-size-typo-base !default;
61
62
  outline: none;
62
63
  }
63
64
 
65
+ .#{$eccgui}-card--whitespace-borderless {
66
+ box-shadow: none;
67
+ }
68
+
64
69
  .#{$eccgui}-card__header {
65
70
  box-sizing: content-box;
66
71
  flex-grow: 0;
@@ -24,7 +24,7 @@ export default {
24
24
 
25
25
  const Template: StoryFn<typeof Icon> = (args) => (
26
26
  <OverlaysProvider>
27
- <Icon {...args} tooltipText={args.name?.toString()} />
27
+ <Icon tooltipText={args.name?.toString()} {...args} />
28
28
  </OverlaysProvider>
29
29
  );
30
30
 
@@ -21,14 +21,9 @@ export interface MultiSelectSelectionProps<T> {
21
21
  // @deprecated use `MultiSelectSelectionProps<T>`
22
22
  export type SelectedParamsType<T> = MultiSelectSelectionProps<T>;
23
23
 
24
- export interface MultiSelectProps<T>
24
+ interface MultiSelectCommonProps<T>
25
25
  extends TestableComponent,
26
26
  Pick<BlueprintMultiSelectProps<T>, "items" | "placeholder" | "openOnKeyDown"> {
27
- /**
28
- * Predefined selected values
29
- */
30
-
31
- selectedItems?: T[];
32
27
  /**
33
28
  * Additional class name, space separated.
34
29
  */
@@ -42,10 +37,7 @@ export interface MultiSelectProps<T>
42
37
  * this would be used in the item selection list as well as the multi-select input
43
38
  */
44
39
  itemLabel: (item: T) => string;
45
- /**
46
- * When set to true will set the multi-select value with all the items provided
47
- */
48
- prePopulateWithItems?: boolean;
40
+
49
41
  /**
50
42
  * function handler that would be called anytime an item is selected/deselected or an item is created/removed
51
43
  */
@@ -127,6 +119,24 @@ export interface MultiSelectProps<T>
127
119
  wrapperProps?: React.HTMLAttributes<HTMLDivElement>;
128
120
  }
129
121
 
122
+ export type MultiSelectProps<T> = MultiSelectCommonProps<T> &
123
+ (
124
+ | {
125
+ /**
126
+ * Predefined selected values
127
+ */
128
+ selectedItems?: T[];
129
+ prePopulateWithItems?: never;
130
+ }
131
+ | {
132
+ selectedItems?: never;
133
+ /**
134
+ * When set to true will set the multi-select value with all the items provided
135
+ */
136
+ prePopulateWithItems?: boolean;
137
+ }
138
+ );
139
+
130
140
  /**
131
141
  * **Element is deprecated for the current type of usage.**
132
142
  * Use `MultiSuggestField` as replacement.
@@ -169,7 +179,7 @@ export function MultiSelect<T>({
169
179
  const [externalItems, setExternalItems] = React.useState<T[]>([...items]);
170
180
  // All options (created and passed) that match the query
171
181
  const [filteredItems, setFilteredItems] = React.useState<T[]>([]);
172
- // All options (created and passed) selected by a user, if the component is uncontrolled
182
+ // All options (created and passed) selected by a user
173
183
  const [selectedItems, setSelectedItems] = React.useState<T[]>(() =>
174
184
  prePopulateWithItems ? [...items] : externalSelectedItems ? [...externalSelectedItems] : []
175
185
  );
@@ -201,10 +211,6 @@ export function MultiSelect<T>({
201
211
  break;
202
212
  }
203
213
 
204
- // If the component is contolled from outside, we don't need to store selected state within the component
205
- // when user selects or removes selection - options will be set in a parent component
206
- const isControlled = !!(externalSelectedItems && onSelection);
207
-
208
214
  /** Update external items when they change
209
215
  * e.g for auto-complete when query change
210
216
  */
@@ -214,15 +220,13 @@ export function MultiSelect<T>({
214
220
  }, [items.map((item) => itemId(item)).join("|")]);
215
221
 
216
222
  React.useEffect(() => {
217
- !isControlled &&
218
- onSelection &&
223
+ onSelection &&
219
224
  onSelection({
220
225
  newlySelected: selectedItems.slice(-1)[0],
221
226
  createdItems: createdItems.current,
222
227
  selectedItems,
223
228
  });
224
229
  }, [
225
- isControlled,
226
230
  onSelection,
227
231
  selectedItems.map((item) => itemId(item)).join("|"),
228
232
  createdItems.current.map((item) => itemId(item)).join("|"),
@@ -237,7 +241,7 @@ export function MultiSelect<T>({
237
241
  }
238
242
 
239
243
  setSelectedItems(externalSelectedItems);
240
- }, [externalSelectedItems]);
244
+ }, [externalSelectedItems?.map((item) => itemId(item)).join("|")]);
241
245
 
242
246
  /**
243
247
  * using the equality prop specified checks if an item has already been selected
@@ -254,15 +258,7 @@ export function MultiSelect<T>({
254
258
  */
255
259
  const removeItemSelection = (matcher: string) => {
256
260
  const filteredItems = selectedItems.filter((item) => itemId(item) !== matcher);
257
-
258
- if (isControlled) {
259
- onSelection({
260
- createdItems: createdItems.current,
261
- selectedItems: filteredItems,
262
- });
263
- } else {
264
- setSelectedItems(filteredItems);
265
- }
261
+ setSelectedItems(filteredItems);
266
262
  };
267
263
 
268
264
  /**
@@ -273,12 +269,6 @@ export function MultiSelect<T>({
273
269
  const onItemSelect = (item: T) => {
274
270
  if (itemHasBeenSelectedAlready(itemId(item))) {
275
271
  removeItemSelection(itemId(item));
276
- } else if (isControlled) {
277
- onSelection({
278
- newlySelected: item,
279
- createdItems: createdItems.current,
280
- selectedItems: [...selectedItems, item],
281
- });
282
272
  } else {
283
273
  setSelectedItems((items) => [...items, item]);
284
274
  }
@@ -358,15 +348,7 @@ export function MultiSelect<T>({
358
348
  const handleClear = () => {
359
349
  requestState.current.query = "";
360
350
 
361
- if (isControlled) {
362
- onSelection({
363
- selectedItems: [],
364
- createdItems: createdItems.current,
365
- });
366
- } else {
367
- setSelectedItems([]);
368
- }
369
-
351
+ setSelectedItems([]);
370
352
  setFilteredItems([...externalItems, ...createdItems.current]);
371
353
  };
372
354
 
@@ -460,6 +442,11 @@ export function MultiSelect<T>({
460
442
 
461
443
  const contentMultiSelect = (
462
444
  <BlueprintMultiSelect<T>
445
+ placeholder={
446
+ !otherMultiSelectProps.placeholder && createNewItemFromQuery
447
+ ? "Search for item, or enter term to create new one..."
448
+ : undefined
449
+ }
463
450
  {...otherMultiSelectProps}
464
451
  query={requestState.current.query}
465
452
  onQueryChange={onQueryChange}
@@ -125,7 +125,7 @@ uncontrolledNewItemCreation.args = {
125
125
  };
126
126
 
127
127
  const CreationTemplate: StoryFn = () => {
128
- const [selectedValues, setSelectedValues] = useState<string[]>([]);
128
+ const [selectedValues, setSelectedValues] = useState<string[]>(["foo"]);
129
129
 
130
130
  const items = useMemo<string[]>(() => ["foo", "bar", "baz"], []);
131
131
 
@@ -145,7 +145,6 @@ const CreationTemplate: StoryFn = () => {
145
145
  itemId={identity}
146
146
  itemLabel={identity}
147
147
  createNewItemFromQuery={identity}
148
- prePopulateWithItems
149
148
  />
150
149
  );
151
150
  };
@@ -47,7 +47,7 @@ export const TestComponent = (): JSX.Element => {
47
47
  };
48
48
 
49
49
  describe("MultiSuggestField", () => {
50
- describe("uncontrolled", () => {
50
+ describe("uncontrolled (when only selectedItems or onSelect is provided)", () => {
51
51
  it("should render default input", () => {
52
52
  const { container } = render(<MultiSuggestField {...Default.args} />);
53
53
  const [input] = container.getElementsByClassName("eccgui-multiselect");
@@ -262,7 +262,7 @@ describe("MultiSuggestField", () => {
262
262
  });
263
263
  });
264
264
 
265
- describe("controlled", () => {
265
+ describe("controlled (when both selectedItems and onSelect are provided)", () => {
266
266
  it("should render default selected items", async () => {
267
267
  const onSelection = jest.fn();
268
268
 
@@ -287,7 +287,12 @@ describe("MultiSuggestField", () => {
287
287
  });
288
288
 
289
289
  const { container } = render(
290
- <MultiSuggestField {...dropdownOnFocus.args} items={items} onSelection={onSelection} />
290
+ <MultiSuggestField
291
+ {...dropdownOnFocus.args}
292
+ items={items}
293
+ selectedItems={[]}
294
+ onSelection={onSelection}
295
+ />
291
296
  );
292
297
 
293
298
  const [inputContainer] = container.getElementsByClassName("eccgui-multiselect");
@@ -355,7 +360,6 @@ describe("MultiSuggestField", () => {
355
360
  newlySelected: items[0],
356
361
  selectedItems: [items[0]],
357
362
  };
358
- expect(onSelection).toHaveBeenCalledTimes(1);
359
363
  expect(onSelection).toHaveBeenCalledWith(expectedObject);
360
364
  });
361
365
 
@@ -381,7 +385,6 @@ describe("MultiSuggestField", () => {
381
385
  selectedItems: [...selectedItems, items[0]],
382
386
  };
383
387
 
384
- expect(onSelection).toHaveBeenCalledTimes(2);
385
388
  expect(onSelection).toHaveBeenCalledWith(expectedObject);
386
389
  });
387
390
 
@@ -399,9 +402,90 @@ describe("MultiSuggestField", () => {
399
402
  selectedItems: [],
400
403
  };
401
404
 
402
- expect(onSelection).toHaveBeenCalledTimes(3);
403
405
  expect(onSelection).toHaveBeenCalledWith(expectedObject);
404
406
  });
405
407
  });
408
+
409
+ it("should set prePopulateWithItems as selected values and override passed values", async () => {
410
+ const onSelection = jest.fn((values) => {
411
+ // eslint-disable-next-line no-console
412
+ console.log("Mocked onSelection function values: ", values);
413
+ });
414
+
415
+ const items = dropdownOnFocus.args.items;
416
+
417
+ const args = { ...dropdownOnFocus.args, onSelection: onSelection };
418
+
419
+ const { container } = render(
420
+ <MultiSuggestField {...args} data-test-id="multi-suggest-field" prePopulateWithItems />
421
+ );
422
+
423
+ await waitFor(() => {
424
+ const expectedObject = {
425
+ createdItems: [],
426
+ newlySelected: items.at(-1),
427
+ selectedItems: items,
428
+ };
429
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
430
+ });
431
+
432
+ const tags = container.querySelectorAll("span[data-tag-index]");
433
+
434
+ expect(tags.length).toBe(items.length);
435
+ });
436
+
437
+ it("should correctly deselect all tags from input", async () => {
438
+ const onSelection = jest.fn((values) => {
439
+ // eslint-disable-next-line no-console
440
+ console.log("Mocked onSelection function values 111: ", values);
441
+ });
442
+
443
+ const items = predefinedNotControlledValues.args.items;
444
+
445
+ const args = { ...predefinedNotControlledValues.args, selectedItems: undefined, onSelection: onSelection };
446
+
447
+ const { container } = render(
448
+ <MultiSuggestField {...args} data-test-id="multi-suggest-field" prePopulateWithItems />
449
+ );
450
+
451
+ await waitFor(() => {
452
+ const expectedObject = {
453
+ createdItems: [],
454
+ newlySelected: items.at(-1),
455
+ selectedItems: items,
456
+ };
457
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
458
+ });
459
+
460
+ let tags = container.querySelectorAll("span[data-tag-index]");
461
+ expect(tags.length).toBe(items.length);
462
+
463
+ for (let i = 0; i < items.length; i += 1) {
464
+ const tag = tags[0];
465
+ expect(tag.querySelector("span")).toHaveTextContent(items[i].testLabel);
466
+
467
+ const removeTagButton = tag.querySelector("button");
468
+ expect(removeTagButton).toBeTruthy();
469
+
470
+ fireEvent.click(removeTagButton!);
471
+
472
+ await waitFor(() => {
473
+ const selected = items.slice(i + 1);
474
+
475
+ const expectedObject = {
476
+ createdItems: [],
477
+ newlySelected: selected.at(-1),
478
+ selectedItems: selected,
479
+ };
480
+
481
+ expect(onSelection).toHaveBeenCalledWith(expectedObject);
482
+ });
483
+
484
+ tags = container.querySelectorAll("span[data-tag-index]");
485
+ }
486
+
487
+ const tagsAfterRemove = container.querySelectorAll("span[data-tag-index]");
488
+ expect(tagsAfterRemove.length).toBe(0);
489
+ });
406
490
  });
407
491
  });