@axinom/mosaic-ui 0.51.0-rc.9 → 0.52.0-rc.0

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 (100) hide show
  1. package/dist/components/Explorer/ConditionalSplit/ConditionalSplit.d.ts +8 -0
  2. package/dist/components/Explorer/ConditionalSplit/ConditionalSplit.d.ts.map +1 -0
  3. package/dist/components/Explorer/Explorer.d.ts +3 -1
  4. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  5. package/dist/components/Explorer/Explorer.model.d.ts +18 -1
  6. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  7. package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts +11 -0
  8. package/dist/components/Explorer/QuickEdit/QuickEditContext.d.ts.map +1 -0
  9. package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts +22 -0
  10. package/dist/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -0
  11. package/dist/components/Explorer/{InMemoryDataProvider.d.ts → helpers/InMemoryDataProvider.d.ts} +3 -3
  12. package/dist/components/Explorer/helpers/InMemoryDataProvider.d.ts.map +1 -0
  13. package/dist/components/Explorer/helpers/useActions.d.ts +31 -0
  14. package/dist/components/Explorer/helpers/useActions.d.ts.map +1 -0
  15. package/dist/components/Explorer/{useDataProvider.d.ts → helpers/useDataProvider.d.ts} +6 -6
  16. package/dist/components/Explorer/helpers/useDataProvider.d.ts.map +1 -0
  17. package/dist/components/Explorer/helpers/useFilters.d.ts +21 -0
  18. package/dist/components/Explorer/helpers/useFilters.d.ts.map +1 -0
  19. package/dist/components/Explorer/helpers/useStationMessage.d.ts +17 -0
  20. package/dist/components/Explorer/helpers/useStationMessage.d.ts.map +1 -0
  21. package/dist/components/Explorer/index.d.ts +2 -1
  22. package/dist/components/Explorer/index.d.ts.map +1 -1
  23. package/dist/components/FormStation/Create/Create.d.ts.map +1 -1
  24. package/dist/components/FormStation/FormStation.d.ts +4 -1
  25. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  26. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +1 -0
  27. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  28. package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts +11 -0
  29. package/dist/components/FormStation/SaveOnDemand/SaveOnDemand.d.ts.map +1 -0
  30. package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
  31. package/dist/components/Icons/Icons.d.ts.map +1 -1
  32. package/dist/components/Icons/Icons.models.d.ts +28 -24
  33. package/dist/components/Icons/Icons.models.d.ts.map +1 -1
  34. package/dist/components/List/List.d.ts +1 -1
  35. package/dist/components/List/List.d.ts.map +1 -1
  36. package/dist/components/List/List.model.d.ts +4 -0
  37. package/dist/components/List/List.model.d.ts.map +1 -1
  38. package/dist/components/PageHeader/PageHeader.d.ts.map +1 -1
  39. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  40. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +2 -1
  41. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
  42. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +1 -1
  43. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
  44. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts +6 -0
  45. package/dist/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +1 -0
  46. package/dist/index.es.js +4 -4
  47. package/dist/index.es.js.map +1 -1
  48. package/dist/index.js +4 -4
  49. package/dist/index.js.map +1 -1
  50. package/dist/initialize.d.ts +1 -1
  51. package/dist/initialize.d.ts.map +1 -1
  52. package/package.json +4 -3
  53. package/src/components/EmptyStation/EmptyStation.spec.tsx +24 -0
  54. package/src/components/Explorer/ConditionalSplit/ConditionalSplit.tsx +23 -0
  55. package/src/components/Explorer/Explorer.model.ts +19 -1
  56. package/src/components/Explorer/Explorer.scss +4 -0
  57. package/src/components/Explorer/Explorer.spec.tsx +28 -3
  58. package/src/components/Explorer/Explorer.stories.tsx +90 -5
  59. package/src/components/Explorer/Explorer.tsx +149 -185
  60. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.spec.tsx +26 -0
  61. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +2 -2
  62. package/src/components/Explorer/QuickEdit/QuickEditContext.tsx +16 -0
  63. package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +461 -0
  64. package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +169 -0
  65. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +6 -0
  66. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.stories.tsx +2 -2
  67. package/src/components/Explorer/{InMemoryDataProvider.ts → helpers/InMemoryDataProvider.ts} +4 -4
  68. package/src/components/Explorer/helpers/useActions.ts +203 -0
  69. package/src/components/Explorer/{useDataProvider.tsx → helpers/useDataProvider.tsx} +11 -11
  70. package/src/components/Explorer/helpers/useFilters.tsx +77 -0
  71. package/src/components/Explorer/{useStationMessage.tsx → helpers/useStationMessage.tsx} +8 -6
  72. package/src/components/Explorer/index.ts +10 -6
  73. package/src/components/FormStation/Create/Create.tsx +1 -0
  74. package/src/components/FormStation/FormStation.spec.tsx +62 -73
  75. package/src/components/FormStation/FormStation.tsx +31 -15
  76. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +38 -18
  77. package/src/components/FormStation/SaveOnDemand/SaveOnDemand.tsx +55 -0
  78. package/src/components/FormStation/helpers/useDataProvider.ts +1 -8
  79. package/src/components/Icons/Icons.models.ts +4 -0
  80. package/src/components/Icons/Icons.tsx +78 -0
  81. package/src/components/InlineMenu/InlineMenu.spec.tsx +18 -0
  82. package/src/components/List/List.model.ts +5 -0
  83. package/src/components/List/List.tsx +29 -5
  84. package/src/components/List/ListRow/ListRow.spec.tsx +0 -10
  85. package/src/components/List/ListRow/ListRow.tsx +1 -1
  86. package/src/components/PageHeader/PageHeader.scss +1 -2
  87. package/src/components/PageHeader/PageHeader.stories.tsx +6 -2
  88. package/src/components/PageHeader/PageHeader.tsx +10 -16
  89. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +1 -0
  90. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.scss +7 -0
  91. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +1 -0
  92. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +19 -7
  93. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +19 -12
  94. package/src/components/PageHeader/helpers/useElementWidthObserver.tsx +30 -0
  95. package/src/initialize.ts +2 -2
  96. package/dist/components/Explorer/InMemoryDataProvider.d.ts.map +0 -1
  97. package/dist/components/Explorer/useDataProvider.d.ts.map +0 -1
  98. package/dist/components/Explorer/useStationMessage.d.ts +0 -15
  99. package/dist/components/Explorer/useStationMessage.d.ts.map +0 -1
  100. /package/src/components/Explorer/{InMemoryDataProvider.spec.ts → helpers/InMemoryDataProvider.spec.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { FormikValues, useFormikContext } from 'formik';
2
- import React, { useEffect, useMemo } from 'react';
2
+ import React, { useContext, useMemo } from 'react';
3
3
  import { useHistory } from 'react-router-dom';
4
- import { SaveIndicatorType, setSaveIndicator } from '../../../initialize';
4
+ import { QuickEditContext } from '../../Explorer';
5
5
  import { IconName } from '../../Icons';
6
6
  import {
7
7
  PageHeader,
@@ -20,6 +20,7 @@ export const FormStationHeader: React.FC<
20
20
  defaultTitle?: string;
21
21
  cancelNavigationUrl?: string;
22
22
  setTabTitle?: boolean;
23
+ showSaveHeaderAction?: boolean;
23
24
  }
24
25
  > = ({
25
26
  titleProperty,
@@ -28,23 +29,10 @@ export const FormStationHeader: React.FC<
28
29
  cancelNavigationUrl,
29
30
  className,
30
31
  setTabTitle,
32
+ showSaveHeaderAction,
31
33
  }) => {
32
34
  const { dirty, resetForm } = useFormikContext<FormikValues>();
33
-
34
- useEffect(() => {
35
- // Set the save indicator to dirty depending on the form state
36
- if (dirty) {
37
- setSaveIndicator(SaveIndicatorType.Dirty);
38
- } else {
39
- setSaveIndicator(SaveIndicatorType.Inactive);
40
- }
41
- return () => {
42
- // The form is not always considered "not dirty" after the save
43
- // so this code will make sure that the indicator is set to inactive
44
- // when the station is left.
45
- setSaveIndicator(SaveIndicatorType.Inactive);
46
- };
47
- }, [dirty]);
35
+ const quickEditContext = useContext(QuickEditContext);
48
36
 
49
37
  const history = useHistory();
50
38
 
@@ -63,6 +51,31 @@ export const FormStationHeader: React.FC<
63
51
  resetForm();
64
52
  },
65
53
  });
54
+
55
+ if (showSaveHeaderAction) {
56
+ actionItems.push({
57
+ label: 'Save',
58
+ icon: IconName.Save,
59
+ kind: 'action',
60
+ actionType: PageHeaderActionType.Context,
61
+ onClick: () => {
62
+ if (quickEditContext?.isQuickEditMode) {
63
+ quickEditContext.refresh();
64
+ } else {
65
+ history.replace(history.location.pathname);
66
+ }
67
+ },
68
+ });
69
+ }
70
+ }
71
+
72
+ if (quickEditContext?.detailsLink !== undefined) {
73
+ actionItems.push({
74
+ label: 'Open',
75
+ icon: IconName.ChevronRight,
76
+ kind: 'action',
77
+ path: quickEditContext.detailsLink,
78
+ });
66
79
  }
67
80
 
68
81
  if (cancelNavigationUrl) {
@@ -79,7 +92,14 @@ export const FormStationHeader: React.FC<
79
92
  }
80
93
 
81
94
  return actionItems;
82
- }, [cancelNavigationUrl, dirty, history, resetForm]);
95
+ }, [
96
+ cancelNavigationUrl,
97
+ dirty,
98
+ history,
99
+ quickEditContext,
100
+ resetForm,
101
+ showSaveHeaderAction,
102
+ ]);
83
103
 
84
104
  return (
85
105
  <PageHeader
@@ -0,0 +1,55 @@
1
+ import { useFormikContext } from 'formik';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ interface SaveOnDemandProps {
5
+ /** If set to true, will prevent form submission when navigating away. (default: false) */
6
+ isSubmitting?: boolean;
7
+ /** Callback that will be called when the form is invalid */
8
+ onFormInvalid?: () => void;
9
+
10
+ registerSaveCallback?: (callback: () => Promise<void>) => void;
11
+ }
12
+
13
+ export const SaveOnDemand: React.FC<SaveOnDemandProps> = ({
14
+ onFormInvalid,
15
+ isSubmitting,
16
+ registerSaveCallback,
17
+ }) => {
18
+ const { submitForm, dirty, isValid } = useFormikContext();
19
+ const [canSubmit, setCanSubmit] = useState<boolean>(true);
20
+
21
+ useEffect(() => {
22
+ const saveCallback = async (): Promise<void> => {
23
+ if (!dirty) {
24
+ return;
25
+ }
26
+
27
+ if (!isValid) {
28
+ onFormInvalid?.();
29
+ throw new Error('Form is not valid');
30
+ }
31
+
32
+ try {
33
+ if (!isSubmitting && canSubmit) {
34
+ setCanSubmit(false);
35
+ await submitForm();
36
+ }
37
+ } catch (error) {
38
+ setCanSubmit(true);
39
+ throw error;
40
+ }
41
+ };
42
+
43
+ registerSaveCallback?.(saveCallback);
44
+ }, [
45
+ canSubmit,
46
+ dirty,
47
+ isSubmitting,
48
+ isValid,
49
+ onFormInvalid,
50
+ registerSaveCallback,
51
+ submitForm,
52
+ ]);
53
+
54
+ return null;
55
+ };
@@ -7,11 +7,7 @@ import {
7
7
  useRef,
8
8
  useState,
9
9
  } from 'react';
10
- import {
11
- SaveIndicatorType,
12
- setSaveIndicator,
13
- showNotification,
14
- } from '../../../initialize';
10
+ import { showNotification } from '../../../initialize';
15
11
  import { Data } from '../../../types';
16
12
  import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
17
13
  import { ErrorType } from '../../models';
@@ -88,7 +84,6 @@ export const useDataProvider: FormStationDataProvider = <
88
84
 
89
85
  try {
90
86
  setIsFormSubmitting(true);
91
- setSaveIndicator(SaveIndicatorType.Saving);
92
87
  setStationError(undefined);
93
88
  if (!initialData.loading && saveData) {
94
89
  const response = await saveData(values, initialData, formikHelpers);
@@ -112,8 +107,6 @@ export const useDataProvider: FormStationDataProvider = <
112
107
  ),
113
108
  );
114
109
 
115
- setSaveIndicator(SaveIndicatorType.Dirty);
116
-
117
110
  // We still throw the error, to make sure that navigation or action execution
118
111
  // will not continue after a failed save.
119
112
  throw error;
@@ -22,6 +22,7 @@ export enum IconName {
22
22
  Error,
23
23
  External,
24
24
  File,
25
+ Filters,
25
26
  ForwardOne,
26
27
  ForwardTen,
27
28
  Info,
@@ -34,9 +35,12 @@ export enum IconName {
34
35
  Play,
35
36
  Plus,
36
37
  Publish,
38
+ QuickEdit,
39
+ QuickEditStation,
37
40
  RemoveFilter,
38
41
  Retry,
39
42
  Replace,
43
+ Save,
40
44
  Snapshot,
41
45
  Stop,
42
46
  Success,
@@ -331,6 +331,22 @@ const FileIcon: React.FC<{ className?: string }> = ({ className }) => (
331
331
  </svg>
332
332
  );
333
333
 
334
+ const FiltersIcon: React.FC<{ className?: string }> = ({ className }) => (
335
+ <svg
336
+ className={clsx(classes.icons, className)}
337
+ version="1.1"
338
+ xmlns="http://www.w3.org/2000/svg"
339
+ viewBox="0 0 40 40"
340
+ >
341
+ <path
342
+ vectorEffect="non-scaling-stroke"
343
+ fill="none"
344
+ strokeWidth="2"
345
+ d="M5.7,4.7h28.6l-11.3,12v13.6l-6.5,5.1v-18.4L5.7,4.7Z"
346
+ />
347
+ </svg>
348
+ );
349
+
334
350
  const ForwardOne: React.FC<{ className?: string }> = ({ className }) => (
335
351
  <svg
336
352
  className={clsx(classes.icons, className)}
@@ -528,6 +544,40 @@ const PublishIcon: React.FC<{ className?: string }> = ({ className }) => (
528
544
  </svg>
529
545
  );
530
546
 
547
+ const QuickEditIcon: React.FC<{ className?: string }> = ({ className }) => (
548
+ <svg
549
+ className={clsx(classes.icons, className)}
550
+ version="1.1"
551
+ xmlns="http://www.w3.org/2000/svg"
552
+ viewBox="0 0 40 40"
553
+ >
554
+ <path
555
+ vectorEffect="non-scaling-stroke"
556
+ fill="none"
557
+ strokeWidth="2"
558
+ d="M11.8,29.7l-8.8,2.6,2.6-8.8,15.8-15.8,6.2,6.2-15.8,15.8h0ZM27.5,19.7h10.5M22.7,24.3h8.4M18,29h6.2M5.6,23.5l6.2,6.2"
559
+ />
560
+ </svg>
561
+ );
562
+
563
+ const QuickEditStationIcon: React.FC<{ className?: string }> = ({
564
+ className,
565
+ }) => (
566
+ <svg
567
+ className={clsx(classes.icons, className)}
568
+ version="1.1"
569
+ xmlns="http://www.w3.org/2000/svg"
570
+ viewBox="0 0 40 40"
571
+ >
572
+ <path
573
+ vectorEffect="non-scaling-stroke"
574
+ fill="none"
575
+ strokeWidth="2"
576
+ d="M7.5,4.5h25v31H7.5V4.5ZM17.82,13.14h8.87M12.98,13.14h3.07M12.98,17.71h3.07M12.98,22.29h3.07M12.98,26.86h3.07M17.82,17.71h8.87M17.82,26.86h8.87M17.82,22.29h4.84"
577
+ />
578
+ </svg>
579
+ );
580
+
531
581
  const RemoveFilterIcon: React.FC<{ className?: string }> = ({ className }) => (
532
582
  <svg
533
583
  className={clsx(classes.icons, className)}
@@ -581,6 +631,30 @@ const RetryIcon: React.FC<{ className?: string }> = ({ className }) => (
581
631
  </svg>
582
632
  );
583
633
 
634
+ const SaveIcon: React.FC<{ className?: string }> = ({ className }) => (
635
+ <svg
636
+ className={clsx(classes.icons, className)}
637
+ version="1.1"
638
+ xmlns="http://www.w3.org/2000/svg"
639
+ viewBox="0 0 40 40"
640
+ >
641
+ <path
642
+ vectorEffect="non-scaling-stroke"
643
+ fill="none"
644
+ strokeWidth="2"
645
+ d="M15.8,30.1c0,0-6.2,0.1-7.6-0.1c-3.3-0.4-5.8-3.3-5.6-6.7c-0.2-6,6.9-6.6,6.9-6.6
646
+ s-0.7-6.4,5-7.5c3.2-0.8,6.5,0.8,7.8,3.8c1.5-0.6,3.2-0.6,4.7,0.2c1.3,0.8,2.2,2.1,2.4,3.6c0,0,8-0.5,8.1,6.6c0,7.1-6.7,6.7-6.7,6.7
647
+ h-6.5"
648
+ />
649
+ <path
650
+ vectorEffect="non-scaling-stroke"
651
+ fill="none"
652
+ strokeWidth="2"
653
+ d="M20.1,33V19.4 M15.7,23.8l4.3-4.3l4.3,4.3"
654
+ />
655
+ </svg>
656
+ );
657
+
584
658
  const SnapshotIcon: React.FC<{ className?: string }> = ({ className }) => (
585
659
  <svg
586
660
  className={clsx(classes.icons, className)}
@@ -768,6 +842,7 @@ export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
768
842
  [IconName.Error]: <ErrorIcon className={className} />,
769
843
  [IconName.External]: <ExternalIcon className={className} />,
770
844
  [IconName.File]: <FileIcon className={className} />,
845
+ [IconName.Filters]: <FiltersIcon className={className} />,
771
846
  [IconName.ForwardOne]: <ForwardOne className={className} />,
772
847
  [IconName.ForwardTen]: <ForwardTen className={className} />,
773
848
  [IconName.Info]: <InfoIcon className={className} />,
@@ -780,9 +855,12 @@ export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
780
855
  [IconName.Play]: <PlayIcon className={className} />,
781
856
  [IconName.Plus]: <PlusIcon className={className} />,
782
857
  [IconName.Publish]: <PublishIcon className={className} />,
858
+ [IconName.QuickEdit]: <QuickEditIcon className={className} />,
859
+ [IconName.QuickEditStation]: <QuickEditStationIcon className={className} />,
783
860
  [IconName.RemoveFilter]: <RemoveFilterIcon className={className} />,
784
861
  [IconName.Replace]: <ReplaceIcon className={className} />,
785
862
  [IconName.Retry]: <RetryIcon className={className} />,
863
+ [IconName.Save]: <SaveIcon className={className} />,
786
864
  [IconName.Snapshot]: <SnapshotIcon className={className} />,
787
865
  [IconName.Stop]: <StopIcon className={className} />,
788
866
  [IconName.Success]: <SuccessIcon className={className} />,
@@ -1,10 +1,28 @@
1
1
  import { mount, shallow } from 'enzyme';
2
2
  import React from 'react';
3
3
  import { act } from 'react-dom/test-utils';
4
+ import { noop } from '../../helpers/utils';
5
+ import { initializeUi } from '../../initialize';
4
6
  import { Button } from '../Buttons';
5
7
  import { InlineMenu } from './InlineMenu';
6
8
 
7
9
  describe('InlineMenu', () => {
10
+ beforeEach(() => {
11
+ initializeUi({
12
+ showNotification: () => {
13
+ // not implemented
14
+ return -1;
15
+ },
16
+ addIndicator: () => {
17
+ return -1;
18
+ },
19
+ removeIndicator: noop,
20
+ on: noop,
21
+ setTitle: noop,
22
+ setSaveIndicator: noop,
23
+ });
24
+ });
25
+
8
26
  it('renders the component without crashing', () => {
9
27
  const wrapper = shallow(<InlineMenu />);
10
28
 
@@ -112,4 +112,9 @@ export interface ListElement {
112
112
  * Resets the selection of the list.
113
113
  */
114
114
  resetSelection: () => void;
115
+
116
+ /**
117
+ * Selects an index in the list.
118
+ */
119
+ selectIndex: (index: number) => void;
115
120
  }
@@ -97,7 +97,7 @@ export interface ListProps<T extends Data> {
97
97
  *
98
98
  * This is not being used, if generateItemLink is set!
99
99
  */
100
- onItemClicked?: (data: T) => void;
100
+ onItemClicked?: (data: T) => void | Promise<void>;
101
101
  /** Raised when item selection has changed */
102
102
  onItemSelected?: (itemSelectEvent: ItemSelectEventArgs<T>) => void;
103
103
  /** Raised when list has scrolled down to the item indicated by loadingTriggerOffset */
@@ -235,6 +235,23 @@ const ListRenderer = <T extends Data>(
235
235
  }
236
236
  };
237
237
 
238
+ const itemClickedHandler = async (data: T, index: number): Promise<void> => {
239
+ try {
240
+ await onItemClicked(data);
241
+
242
+ if (selectionMode === ListSelectMode.Single) {
243
+ setListItems((prevState) =>
244
+ prevState.map((item, i) => ({
245
+ selected: i === index,
246
+ data: item.data,
247
+ })),
248
+ );
249
+ }
250
+ } catch (error) {
251
+ return;
252
+ }
253
+ };
254
+
238
255
  const { sort, sortChangedHandler } = useSort(defaultSortOrder, onSortChanged);
239
256
 
240
257
  useImperativeHandle(ref, () => ({
@@ -253,6 +270,13 @@ const ListRenderer = <T extends Data>(
253
270
  items: [],
254
271
  });
255
272
  },
273
+ selectIndex: (index: number) => {
274
+ if (selectionMode === ListSelectMode.Single) {
275
+ itemClickedHandler(listItems[index].data, index);
276
+ } else {
277
+ itemSelectedHandler(true, index);
278
+ }
279
+ },
256
280
  }));
257
281
 
258
282
  return (
@@ -312,12 +336,12 @@ const ListRenderer = <T extends Data>(
312
336
  showCheckMark={selectionMode === ListSelectMode.Single}
313
337
  showItemCheckbox={selectionMode === ListSelectMode.Multi}
314
338
  onItemClicked={
315
- generateItemLink ? generateItemLink(item.data) : onItemClicked
339
+ generateItemLink
340
+ ? generateItemLink(item.data)
341
+ : (data) => itemClickedHandler(data, index)
316
342
  }
317
343
  onTriggered={onTriggeredHandler}
318
- onItemSelected={(checked) => {
319
- itemSelectedHandler(checked, index);
320
- }}
344
+ onItemSelected={(checked) => itemSelectedHandler(checked, index)}
321
345
  inlineMenuActions={inlineMenuActions}
322
346
  />
323
347
  ))}
@@ -338,16 +338,6 @@ describe('ListRow', () => {
338
338
  expect(row.hasClass('selected')).toBe(false);
339
339
  });
340
340
 
341
- it(`does not have the 'selected' class if showItemCheckbox prop is false`, () => {
342
- const wrapper = mount(
343
- <ListRow {...mockProps} itemSelected={true} showItemCheckbox={false} />,
344
- );
345
-
346
- const row = wrapper.find('.columnsRoot');
347
-
348
- expect(row.hasClass('selected')).toBe(false);
349
- });
350
-
351
341
  it(`only has the 'selected' class if both itemSelect and showItemCheckbox props are true`, () => {
352
342
  const wrapper = mount(
353
343
  <ListRow {...mockProps} itemSelected={true} showItemCheckbox={true} />,
@@ -278,7 +278,7 @@ export const ListRow = <T extends Data>({
278
278
  const Row = (
279
279
  <div
280
280
  className={clsx(classes.columnsRoot, {
281
- [classes.selected]: itemSelected && showItemCheckbox,
281
+ [classes.selected]: itemSelected,
282
282
  [classes.disabled]: isRowDisabled,
283
283
  })}
284
284
  style={customRootStyles}
@@ -9,7 +9,7 @@
9
9
  grid-template-rows: 1fr;
10
10
 
11
11
  @media only screen and (min-width: $XL-min-size) {
12
- grid-template-columns: minmax(480px, 1fr) max-content max-content;
12
+ grid-template-columns: minmax(25%, 1fr) max-content max-content;
13
13
  }
14
14
 
15
15
  color: var(--page-header-color, $page-header-color);
@@ -53,7 +53,6 @@
53
53
  .actions {
54
54
  display: grid;
55
55
  grid-auto-flow: column;
56
- grid-auto-columns: minmax(0, min-content);
57
56
  grid-gap: 1px;
58
57
  }
59
58
 
@@ -148,14 +148,18 @@ export const All: StoryObj<typeof PageHeader> = {
148
148
  icon: IconName.Bulk,
149
149
  kind: 'group',
150
150
  actions: bulkHeaderActions,
151
- onActionsGroupToggled: action('onBulkActions1Toggled'),
151
+ onActionsGroupToggled: async (): Promise<void> => {
152
+ action('onBulkActions1Toggled');
153
+ },
152
154
  },
153
155
  {
154
156
  label: 'Bulk Actions 2',
155
157
  icon: IconName.Bulk,
156
158
  kind: 'group',
157
159
  actions: [bulkHeaderActions[0]],
158
- onActionsGroupToggled: action('onBulkActions2Toggled'),
160
+ onActionsGroupToggled: async (): Promise<void> => {
161
+ action('onBulkActions2Toggled');
162
+ },
159
163
  },
160
164
  ],
161
165
  },
@@ -1,13 +1,13 @@
1
1
  import clsx from 'clsx';
2
- import React from 'react';
2
+ import React, { useEffect, useState } from 'react';
3
3
  import { useTabTitle } from '../../hooks/useTabTitle/useTabTitle';
4
- import { useWindowSize } from '../../hooks/useWindowSize/useWindowSize';
5
4
  import { PageHeaderProps } from './PageHeader.model';
6
5
  import classes from './PageHeader.scss';
7
6
  import { PageHeaderActionProps } from './PageHeaderAction';
8
7
  import { PageHeaderAction } from './PageHeaderAction/PageHeaderAction';
9
8
  import { PageHeaderActionsGroup } from './PageHeaderActionsGroup/PageHeaderActionsGroup';
10
9
  import { PageHeaderActionsGroupContextProvider } from './PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider';
10
+ import { useElementWidthObserver } from './helpers/useElementWidthObserver';
11
11
 
12
12
  /**
13
13
  * Primary header for stations. Accepts a title, subtitle, actions, and actions groups.
@@ -29,29 +29,23 @@ export const PageHeader: React.FC<PageHeaderProps> = ({
29
29
  setTabTitle = true,
30
30
  }) => {
31
31
  useTabTitle(title, setTabTitle);
32
- const { width } = useWindowSize();
33
- const containerRef = React.useRef<HTMLDivElement>(null);
32
+ const { width, ref } = useElementWidthObserver<HTMLDivElement>();
34
33
 
35
- const [availableActionSpace, setAvailableActionSpace] =
36
- React.useState<number>(0);
34
+ const [availableActionSpace, setAvailableActionSpace] = useState<number>(0);
37
35
 
38
- React.useEffect(() => {
39
- if (containerRef.current) {
40
- // Use up to 75% of the container width for actions
41
- const maxActionsWidth = containerRef.current.clientWidth * 0.75;
36
+ useEffect(() => {
37
+ // Use up to 75% of the container width for actions
38
+ const maxActionsWidth = width * 0.75;
42
39
 
43
- // Each action is 120px wide
44
- setAvailableActionSpace(
45
- Math.floor(maxActionsWidth / 120 - actions.length),
46
- );
47
- }
40
+ // Each action is 120px wide
41
+ setAvailableActionSpace(Math.floor(maxActionsWidth / 120 - actions.length));
48
42
  }, [width, actions.length]);
49
43
 
50
44
  return (
51
45
  <div
52
46
  className={clsx(classes.container, 'page-header-container', className)}
53
47
  data-test-id="page-header"
54
- ref={containerRef}
48
+ ref={ref}
55
49
  >
56
50
  <div className={clsx(classes.titles)}>
57
51
  <div className={clsx(classes.title)} data-test-id="page-header-title">
@@ -5,6 +5,7 @@ import { ConfirmAction, DefaultHandler, LinkAction } from '../../models';
5
5
  export enum PageHeaderActionType {
6
6
  Active,
7
7
  Context,
8
+ Hightlight,
8
9
  }
9
10
 
10
11
  export type PageHeaderActionProps =
@@ -88,6 +88,13 @@
88
88
  }
89
89
  }
90
90
 
91
+ &.highlight {
92
+ background-color: var(
93
+ --page-header-action-context-hover-background-color,
94
+ $page-header-action-context-hover-background-color
95
+ );
96
+ }
97
+
91
98
  &.active {
92
99
  color: var(
93
100
  --page-header-action-context-background-color,
@@ -119,6 +119,7 @@ const PageHeaderJSAction: React.FC<PageHeaderJsActionProps> = ({
119
119
  {
120
120
  [classes.context]: actionType === PageHeaderActionType.Context,
121
121
  [classes.active]: actionType === PageHeaderActionType.Active,
122
+ [classes.highlight]: actionType === PageHeaderActionType.Hightlight,
122
123
  [classes.hasConfirm]: confirmation,
123
124
  },
124
125
  'page-header-action-container',
@@ -1,5 +1,6 @@
1
1
  import { mount, shallow } from 'enzyme';
2
2
  import React from 'react';
3
+ import { act } from 'react-dom/test-utils';
3
4
  import { noop } from '../../../helpers/utils';
4
5
  import { PageHeaderAction } from '../PageHeaderAction/PageHeaderAction';
5
6
  import {
@@ -45,16 +46,18 @@ describe('PageHeaderActionsGroup', () => {
45
46
  expect(wrapper).toBeTruthy();
46
47
  });
47
48
 
48
- it(`'Group Actions' has the 'Context' actionType when closed and 'Active' actionType when open`, () => {
49
+ it(`'Group Actions' has the 'Context' actionType when closed and 'Active' actionType when open`, async () => {
49
50
  const wrapper = mount(<PageHeaderActionsGroup {...defaultProps} />);
50
51
  let groupActionsToggle = wrapper.find(PageHeaderAction).first();
51
- // let action = wrapper.find(PageHeaderAction);
52
52
 
53
53
  expect(groupActionsToggle.prop('actionType')).toBe(
54
54
  PageHeaderActionType.Context,
55
55
  );
56
56
 
57
- groupActionsToggle.simulate('click');
57
+ await act(async () => {
58
+ await groupActionsToggle.prop('onClick')?.();
59
+ wrapper.update();
60
+ });
58
61
 
59
62
  groupActionsToggle = wrapper.find(PageHeaderAction).first();
60
63
 
@@ -63,19 +66,23 @@ describe('PageHeaderActionsGroup', () => {
63
66
  );
64
67
  });
65
68
 
66
- it(`renders all actions when 'Group Actions' is selected and there is enough available action slots`, () => {
69
+ it(`renders all actions when 'Group Actions' is selected and there is enough available action slots`, async () => {
67
70
  const wrapper = mount(
68
71
  <PageHeaderActionsGroup {...defaultProps} availableActionSpace={5} />,
69
72
  );
70
73
  const groupActionsToggle = wrapper.find(PageHeaderAction).first();
71
74
  let actions = wrapper.find(PageHeaderAction);
72
75
 
73
- groupActionsToggle.simulate('click');
76
+ await act(async () => {
77
+ await groupActionsToggle.prop('onClick')?.();
78
+ wrapper.update();
79
+ });
80
+
74
81
  actions = wrapper.find(PageHeaderAction);
75
82
  expect(actions).toHaveLength(5);
76
83
  });
77
84
 
78
- it(`raises onActionsGroupToggled`, () => {
85
+ it(`raises onActionsGroupToggled`, async () => {
79
86
  const groupActionSpy = jest.fn();
80
87
  const wrapper = mount(
81
88
  <PageHeaderActionsGroup
@@ -84,7 +91,12 @@ describe('PageHeaderActionsGroup', () => {
84
91
  />,
85
92
  );
86
93
  const groupActionsToggle = wrapper.find(PageHeaderAction).first();
87
- groupActionsToggle.simulate('click');
94
+
95
+ await act(async () => {
96
+ await groupActionsToggle.prop('onClick')?.();
97
+ wrapper.update();
98
+ });
99
+
88
100
  expect(groupActionSpy).toHaveBeenCalledTimes(1);
89
101
  expect(groupActionSpy).toHaveBeenCalledWith(true);
90
102
  });