@axinom/mosaic-ui 0.42.0 → 0.43.0-rc.1
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/dist/components/FormStation/FormGrid/FormGrid.d.ts +7 -0
- package/dist/components/FormStation/FormGrid/FormGrid.d.ts.map +1 -0
- package/dist/components/FormStation/FormGrid/index.d.ts +2 -0
- package/dist/components/FormStation/FormGrid/index.d.ts.map +1 -0
- package/dist/components/FormStation/FormStation.d.ts +3 -12
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/FormStation/FormStation.models.d.ts +16 -2
- package/dist/components/FormStation/FormStation.models.d.ts.map +1 -1
- package/dist/components/FormStation/FormStationActions/FormStationActions.d.ts +21 -0
- package/dist/components/FormStation/FormStationActions/FormStationActions.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationActions/index.d.ts +2 -0
- package/dist/components/FormStation/FormStationActions/index.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationContentWrapper/FormStationContentWrapper.d.ts +11 -0
- package/dist/components/FormStation/FormStationContentWrapper/FormStationContentWrapper.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationContentWrapper/index.d.ts +2 -0
- package/dist/components/FormStation/FormStationContentWrapper/index.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +11 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -0
- package/dist/components/FormStation/FormStationHeader/index.d.ts +2 -0
- package/dist/components/FormStation/FormStationHeader/index.d.ts.map +1 -0
- package/dist/components/FormStation/helpers/useDataProvider.d.ts +14 -0
- package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -0
- package/dist/components/FormStation/{useValidationError.d.ts → helpers/useValidationError.d.ts} +1 -1
- package/dist/components/FormStation/helpers/useValidationError.d.ts.map +1 -0
- package/dist/components/FormStation/index.d.ts +2 -1
- package/dist/components/FormStation/index.d.ts.map +1 -1
- package/dist/components/Tabs/Tab/CustomTab.d.ts +3 -0
- package/dist/components/Tabs/Tab/CustomTab.d.ts.map +1 -0
- package/dist/components/Tabs/Tab/index.d.ts +2 -0
- package/dist/components/Tabs/Tab/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/CustomTabList.d.ts +3 -0
- package/dist/components/Tabs/TabList/CustomTabList.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts +3 -0
- package/dist/components/Tabs/TabList/ScrollContainer/ScrollContainer.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts +2 -0
- package/dist/components/Tabs/TabList/ScrollContainer/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts +10 -0
- package/dist/components/Tabs/TabList/ScrollContainer/useScroll.d.ts.map +1 -0
- package/dist/components/Tabs/TabList/index.d.ts +2 -0
- package/dist/components/Tabs/TabList/index.d.ts.map +1 -0
- package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts +3 -0
- package/dist/components/Tabs/TabPanel/CustomTabPanel.d.ts.map +1 -0
- package/dist/components/Tabs/TabPanel/index.d.ts +2 -0
- package/dist/components/Tabs/TabPanel/index.d.ts.map +1 -0
- package/dist/components/Tabs/index.d.ts +5 -0
- package/dist/components/Tabs/index.d.ts.map +1 -0
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/FormStation/FormGrid/FormGrid.scss +10 -0
- package/src/components/FormStation/FormGrid/FormGrid.tsx +25 -0
- package/src/components/FormStation/FormGrid/index.ts +1 -0
- package/src/components/FormStation/FormStation.models.ts +28 -2
- package/src/components/FormStation/FormStation.scss +1 -117
- package/src/components/FormStation/FormStation.stories.tsx +166 -1
- package/src/components/FormStation/FormStation.tsx +39 -388
- package/src/components/FormStation/FormStationActions/FormStationActions.tsx +130 -0
- package/src/components/FormStation/FormStationActions/index.ts +1 -0
- package/src/components/FormStation/FormStationContentWrapper/FormStationContentWrapper.scss +66 -0
- package/src/components/FormStation/FormStationContentWrapper/FormStationContentWrapper.tsx +76 -0
- package/src/components/FormStation/FormStationContentWrapper/index.ts +1 -0
- package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +88 -0
- package/src/components/FormStation/FormStationHeader/index.ts +1 -0
- package/src/components/FormStation/helpers/useDataProvider.ts +124 -0
- package/src/components/FormStation/{useValidationError.tsx → helpers/useValidationError.tsx} +2 -1
- package/src/components/FormStation/index.ts +2 -5
- package/src/components/List/ListRow/Renderers/ExternalLinkRenderer/ExternalLinkRenderer.tsx +1 -1
- package/src/components/Tabs/Tab/CustomTab.scss +42 -0
- package/src/components/Tabs/Tab/CustomTab.tsx +34 -0
- package/src/components/Tabs/Tab/index.ts +1 -0
- package/src/components/Tabs/TabList/CustomTabList.scss +7 -0
- package/src/components/Tabs/TabList/CustomTabList.tsx +15 -0
- package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.scss +34 -0
- package/src/components/Tabs/TabList/ScrollContainer/ScrollContainer.tsx +39 -0
- package/src/components/Tabs/TabList/ScrollContainer/index.ts +1 -0
- package/src/components/Tabs/TabList/ScrollContainer/useScroll.ts +114 -0
- package/src/components/Tabs/TabList/index.ts +1 -0
- package/src/components/Tabs/TabPanel/CustomTabPanel.scss +10 -0
- package/src/components/Tabs/TabPanel/CustomTabPanel.tsx +26 -0
- package/src/components/Tabs/TabPanel/index.ts +1 -0
- package/src/components/Tabs/Tabs.stories.tsx +108 -0
- package/src/components/Tabs/index.ts +4 -0
- package/dist/components/FormStation/StationErrorStateType.d.ts +0 -5
- package/dist/components/FormStation/StationErrorStateType.d.ts.map +0 -1
- package/dist/components/FormStation/useValidationError.d.ts.map +0 -1
- package/src/components/FormStation/StationErrorStateType.tsx +0 -5
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { Form } from 'formik';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Data } from '../../../types';
|
|
5
|
+
import { MessageBar } from '../../MessageBar';
|
|
6
|
+
import { FormGrid } from '../FormGrid';
|
|
7
|
+
import { FormStationProps } from '../FormStation';
|
|
8
|
+
import { StationErrorStateType } from '../FormStation.models';
|
|
9
|
+
import classes from './FormStationContentWrapper.scss';
|
|
10
|
+
|
|
11
|
+
interface FormStationContentWrapperProps
|
|
12
|
+
extends Pick<
|
|
13
|
+
FormStationProps<Data>,
|
|
14
|
+
'stationMessage' | 'edgeToEdgeContent' | 'infoPanel' | 'initialData'
|
|
15
|
+
> {
|
|
16
|
+
stationError?: StationErrorStateType;
|
|
17
|
+
setStationError: React.Dispatch<
|
|
18
|
+
React.SetStateAction<StationErrorStateType | undefined>
|
|
19
|
+
>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FormStationContentWrapper: React.FC<
|
|
23
|
+
FormStationContentWrapperProps
|
|
24
|
+
> = ({
|
|
25
|
+
stationMessage,
|
|
26
|
+
edgeToEdgeContent,
|
|
27
|
+
infoPanel,
|
|
28
|
+
initialData,
|
|
29
|
+
stationError,
|
|
30
|
+
setStationError,
|
|
31
|
+
children,
|
|
32
|
+
}) => (
|
|
33
|
+
<>
|
|
34
|
+
{stationError && (
|
|
35
|
+
<div className={classes.errorMessage}>
|
|
36
|
+
<MessageBar
|
|
37
|
+
type="error"
|
|
38
|
+
title={String(stationError.title)}
|
|
39
|
+
onClose={() => setStationError(undefined)}
|
|
40
|
+
>
|
|
41
|
+
{stationError?.body}
|
|
42
|
+
</MessageBar>
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
{initialData.loading ? (
|
|
46
|
+
// TODO: Loading skeleton of the page
|
|
47
|
+
<></>
|
|
48
|
+
) : initialData.error ||
|
|
49
|
+
initialData.data === null ||
|
|
50
|
+
initialData.entityNotFound ? (
|
|
51
|
+
// Error on loading - we can't show the form
|
|
52
|
+
<div className={classes.loadingError}></div>
|
|
53
|
+
) : (
|
|
54
|
+
<div className={classes.children}>
|
|
55
|
+
<div className={classes.main}>
|
|
56
|
+
<div
|
|
57
|
+
className={clsx(classes.formWrapper, {
|
|
58
|
+
[classes.hasMessage]: stationMessage,
|
|
59
|
+
})}
|
|
60
|
+
>
|
|
61
|
+
{stationMessage && <MessageBar {...stationMessage} />}
|
|
62
|
+
<Form>
|
|
63
|
+
<FormGrid edgeToEdgeContent={edgeToEdgeContent}>
|
|
64
|
+
{children}
|
|
65
|
+
{/* Adding a invisible text input here to prevent the browser from submitting on "Enter" when there is only a single text input field in the form
|
|
66
|
+
See: https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2 */}
|
|
67
|
+
<input type="text" style={{ display: 'none' }} />
|
|
68
|
+
</FormGrid>
|
|
69
|
+
</Form>
|
|
70
|
+
</div>
|
|
71
|
+
{infoPanel}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormStationContentWrapper';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { FormikValues, useFormikContext } from 'formik';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { useHistory } from 'react-router-dom';
|
|
4
|
+
import { SaveIndicatorType, setSaveIndicator } from '../../../initialize';
|
|
5
|
+
import { IconName } from '../../Icons';
|
|
6
|
+
import {
|
|
7
|
+
PageHeader,
|
|
8
|
+
PageHeaderActionType,
|
|
9
|
+
PageHeaderProps,
|
|
10
|
+
} from '../../PageHeader';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles showRefresh and cancel buttons based on form states
|
|
14
|
+
*/
|
|
15
|
+
export const FormStationHeader: React.FC<
|
|
16
|
+
Omit<PageHeaderProps, 'title'> & {
|
|
17
|
+
titleProperty?: string;
|
|
18
|
+
defaultTitle?: string;
|
|
19
|
+
cancelNavigationUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
> = ({
|
|
22
|
+
titleProperty,
|
|
23
|
+
defaultTitle,
|
|
24
|
+
subtitle,
|
|
25
|
+
cancelNavigationUrl,
|
|
26
|
+
className,
|
|
27
|
+
}) => {
|
|
28
|
+
const { dirty, resetForm, values } = useFormikContext<FormikValues>();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// Set the save indicator to dirty depending on the form state
|
|
32
|
+
if (dirty) {
|
|
33
|
+
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
34
|
+
} else {
|
|
35
|
+
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
36
|
+
}
|
|
37
|
+
return () => {
|
|
38
|
+
// The form is not always considered "not dirty" after the save
|
|
39
|
+
// so this code will make sure that the indicator is set to inactive
|
|
40
|
+
// when the station is left.
|
|
41
|
+
setSaveIndicator(SaveIndicatorType.Inactive);
|
|
42
|
+
};
|
|
43
|
+
}, [dirty]);
|
|
44
|
+
|
|
45
|
+
const history = useHistory();
|
|
46
|
+
|
|
47
|
+
const title =
|
|
48
|
+
titleProperty && values[titleProperty] !== ''
|
|
49
|
+
? values[titleProperty]
|
|
50
|
+
: defaultTitle ?? '';
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<PageHeader
|
|
54
|
+
title={title}
|
|
55
|
+
subtitle={subtitle}
|
|
56
|
+
className={className}
|
|
57
|
+
actions={[
|
|
58
|
+
...(dirty === true // add undo action if form as been altered
|
|
59
|
+
? [
|
|
60
|
+
{
|
|
61
|
+
label: 'Undo Changes',
|
|
62
|
+
icon: IconName.Undo,
|
|
63
|
+
actionType: PageHeaderActionType.Context,
|
|
64
|
+
onClick: () => {
|
|
65
|
+
resetForm();
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
: []),
|
|
70
|
+
...(cancelNavigationUrl // add cancel action if applicable
|
|
71
|
+
? [
|
|
72
|
+
{
|
|
73
|
+
label: 'Cancel',
|
|
74
|
+
icon: IconName.X,
|
|
75
|
+
onClick: () => {
|
|
76
|
+
resetForm();
|
|
77
|
+
// If the form has errors, Navigation needs to be wrapped in a promise or timeout.
|
|
78
|
+
Promise.resolve().then(() =>
|
|
79
|
+
history.push(cancelNavigationUrl),
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
]
|
|
84
|
+
: []),
|
|
85
|
+
]}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormStationHeader';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { FormikHelpers } from 'formik';
|
|
2
|
+
import {
|
|
3
|
+
Dispatch,
|
|
4
|
+
SetStateAction,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { SaveIndicatorType, setSaveIndicator } from '../../../initialize';
|
|
11
|
+
import { Data } from '../../../types';
|
|
12
|
+
import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
|
|
13
|
+
import { ErrorType } from '../../models';
|
|
14
|
+
import {
|
|
15
|
+
InitialFormData,
|
|
16
|
+
SaveDataFunction,
|
|
17
|
+
StationErrorStateType,
|
|
18
|
+
} from '../FormStation.models';
|
|
19
|
+
|
|
20
|
+
export type FormStationDataProvider = <TValues extends Data, TSubmitResponse>(
|
|
21
|
+
initialData: InitialFormData<TValues>,
|
|
22
|
+
saveData: SaveDataFunction<TValues, TSubmitResponse>,
|
|
23
|
+
) => {
|
|
24
|
+
onSubmit: (
|
|
25
|
+
values: TValues,
|
|
26
|
+
formikHelpers: FormikHelpers<TValues>,
|
|
27
|
+
) => Promise<void>;
|
|
28
|
+
stationError?: StationErrorStateType;
|
|
29
|
+
setStationError: Dispatch<SetStateAction<StationErrorStateType | undefined>>;
|
|
30
|
+
isFormSubmitting: boolean;
|
|
31
|
+
lastSubmittedResponse: React.MutableRefObject<TSubmitResponse | undefined>;
|
|
32
|
+
initialValues: TValues | null | undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const useDataProvider: FormStationDataProvider = <
|
|
36
|
+
TValues extends Data,
|
|
37
|
+
TSubmitResponse,
|
|
38
|
+
>(
|
|
39
|
+
initialData: InitialFormData<TValues>,
|
|
40
|
+
saveData: SaveDataFunction<TValues, TSubmitResponse>,
|
|
41
|
+
) => {
|
|
42
|
+
const [stationError, setStationError] = useState<StationErrorStateType>();
|
|
43
|
+
const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
|
|
44
|
+
const lastSubmittedResponse = useRef<TSubmitResponse>();
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (
|
|
48
|
+
initialData.error ||
|
|
49
|
+
(initialData.data === null && !initialData.loading) ||
|
|
50
|
+
initialData.entityNotFound
|
|
51
|
+
) {
|
|
52
|
+
const stationError = {
|
|
53
|
+
...ErrorTypeToStationError(
|
|
54
|
+
initialData.error,
|
|
55
|
+
'An error occurred when trying to load data.',
|
|
56
|
+
'Entity not found',
|
|
57
|
+
),
|
|
58
|
+
type: 'loading',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
setStationError(stationError);
|
|
62
|
+
} else {
|
|
63
|
+
if (stationError?.type === 'loading') {
|
|
64
|
+
// Only clear the error if it is a loading error, which now seems to be cleared.
|
|
65
|
+
setStationError(undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}, [
|
|
69
|
+
initialData.loading,
|
|
70
|
+
initialData.error,
|
|
71
|
+
initialData.entityNotFound,
|
|
72
|
+
initialData.data,
|
|
73
|
+
stationError?.type,
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const onSubmit = useCallback(
|
|
77
|
+
async (
|
|
78
|
+
values: TValues,
|
|
79
|
+
formikHelpers: FormikHelpers<TValues>,
|
|
80
|
+
): Promise<void> => {
|
|
81
|
+
if (isFormSubmitting) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
setIsFormSubmitting(true);
|
|
87
|
+
setSaveIndicator(SaveIndicatorType.Saving);
|
|
88
|
+
setStationError(undefined);
|
|
89
|
+
if (!initialData.loading && saveData) {
|
|
90
|
+
const response = await saveData(values, initialData, formikHelpers);
|
|
91
|
+
if (response) {
|
|
92
|
+
lastSubmittedResponse.current = response;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
setStationError(
|
|
97
|
+
ErrorTypeToStationError(
|
|
98
|
+
error as ErrorType,
|
|
99
|
+
'An error occurred when trying to save data.',
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
setSaveIndicator(SaveIndicatorType.Dirty);
|
|
104
|
+
|
|
105
|
+
// We still throw the error, to make sure that navigation or action execution
|
|
106
|
+
// will not continue after a failed save.
|
|
107
|
+
throw error;
|
|
108
|
+
} finally {
|
|
109
|
+
formikHelpers.setSubmitting(false);
|
|
110
|
+
setIsFormSubmitting(false);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[isFormSubmitting, initialData, saveData, setStationError],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
onSubmit,
|
|
118
|
+
stationError,
|
|
119
|
+
setStationError,
|
|
120
|
+
isFormSubmitting,
|
|
121
|
+
lastSubmittedResponse,
|
|
122
|
+
initialValues: initialData.data,
|
|
123
|
+
};
|
|
124
|
+
};
|
package/src/components/FormStation/{useValidationError.tsx → helpers/useValidationError.tsx}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useFormikContext } from 'formik';
|
|
2
2
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
3
|
-
import { StationErrorStateType } from '
|
|
3
|
+
import { StationErrorStateType } from '../FormStation.models';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Component that watches for changes in the form validation state
|
|
@@ -15,6 +15,7 @@ const ValidationWatcher: React.FC<{
|
|
|
15
15
|
}, [formik.isValid, isValid]);
|
|
16
16
|
return null;
|
|
17
17
|
};
|
|
18
|
+
|
|
18
19
|
/**
|
|
19
20
|
* Cares for showing (and removing) validation errors.
|
|
20
21
|
* @param stationError the currently showing error
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
export { Create, CreateProps } from './Create/Create';
|
|
2
2
|
export { Details, DetailsProps } from './Details/Details';
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
FormStationProps,
|
|
6
|
-
ObjectSchemaDefinition,
|
|
7
|
-
} from './FormStation';
|
|
3
|
+
export { FormGrid } from './FormGrid';
|
|
4
|
+
export { FormStation, FormStationProps } from './FormStation';
|
|
8
5
|
export * from './FormStation.models';
|
|
@@ -55,10 +55,10 @@ export const createExternalLinkRenderer = <T extends Data>(
|
|
|
55
55
|
rel="noopener noreferrer"
|
|
56
56
|
onClick={handleClick}
|
|
57
57
|
>
|
|
58
|
+
<p title={value}>{value}</p>
|
|
58
59
|
{showNewTabIcon && (
|
|
59
60
|
<Icons icon={IconName.External} className={classes.externalIcon} />
|
|
60
61
|
)}
|
|
61
|
-
<p title={value}>{value}</p>
|
|
62
62
|
</a>
|
|
63
63
|
);
|
|
64
64
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@import '../../../styles/common.scss';
|
|
2
|
+
|
|
3
|
+
.tab {
|
|
4
|
+
@include boxSizing;
|
|
5
|
+
|
|
6
|
+
display: grid;
|
|
7
|
+
height: 49px;
|
|
8
|
+
min-width: 180px;
|
|
9
|
+
background-color: $blue;
|
|
10
|
+
color: white;
|
|
11
|
+
font-size: 16px;
|
|
12
|
+
border: 1px solid $blue;
|
|
13
|
+
border-bottom: none;
|
|
14
|
+
|
|
15
|
+
padding: 0 15px;
|
|
16
|
+
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
|
|
19
|
+
&.selected {
|
|
20
|
+
background: white;
|
|
21
|
+
color: $dark-gray;
|
|
22
|
+
border: 1px solid $light-gray;
|
|
23
|
+
border-bottom: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.disabled {
|
|
27
|
+
background-color: $dark-gray;
|
|
28
|
+
cursor: default;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&:focus {
|
|
32
|
+
outline: none;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.content {
|
|
37
|
+
display: grid;
|
|
38
|
+
align-items: center;
|
|
39
|
+
height: 100%;
|
|
40
|
+
width: 100%;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { ReactTabsFunctionComponent, Tab, TabProps } from 'react-tabs';
|
|
3
|
+
import classes from './CustomTab.scss';
|
|
4
|
+
|
|
5
|
+
export const CustomTab: ReactTabsFunctionComponent<TabProps> = ({
|
|
6
|
+
children,
|
|
7
|
+
...otherProps
|
|
8
|
+
}) => {
|
|
9
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (ref.current && ref.current.parentElement && otherProps.selected) {
|
|
13
|
+
ref.current.parentElement.scrollIntoView({
|
|
14
|
+
behavior: 'smooth',
|
|
15
|
+
block: 'nearest',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}, [otherProps.selected]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Tab
|
|
22
|
+
{...otherProps}
|
|
23
|
+
className={classes.tab}
|
|
24
|
+
selectedClassName={classes.selected}
|
|
25
|
+
data-test-id="tab"
|
|
26
|
+
>
|
|
27
|
+
<div className={classes.content} ref={ref}>
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
</Tab>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
CustomTab.tabsRole = 'Tab';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CustomTab as Tab } from './CustomTab';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ReactTabsFunctionComponent, TabList, TabListProps } from 'react-tabs';
|
|
3
|
+
import classes from './CustomTabList.scss';
|
|
4
|
+
import { ScrollContainer } from './ScrollContainer';
|
|
5
|
+
|
|
6
|
+
export const CustomTabList: ReactTabsFunctionComponent<TabListProps> = ({
|
|
7
|
+
children,
|
|
8
|
+
...otherProps
|
|
9
|
+
}) => (
|
|
10
|
+
<TabList {...otherProps} className={classes.tablist}>
|
|
11
|
+
<ScrollContainer>{children}</ScrollContainer>
|
|
12
|
+
</TabList>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CustomTabList.tabsRole = 'TabList';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@import '../../../../styles/common.scss';
|
|
2
|
+
|
|
3
|
+
.tablistWrapper {
|
|
4
|
+
overflow-x: auto;
|
|
5
|
+
overflow-y: hidden;
|
|
6
|
+
display: grid;
|
|
7
|
+
width: 100%;
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
grid-auto-flow: column;
|
|
10
|
+
grid-auto-columns: minmax(max-content, auto);
|
|
11
|
+
gap: 2px;
|
|
12
|
+
|
|
13
|
+
-ms-overflow-style: none; /* IE and Edge */
|
|
14
|
+
scrollbar-width: none; /* Firefox */
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Hide scrollbar for Chrome, Safari and Opera */
|
|
18
|
+
.tablistWrapper::-webkit-scrollbar {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.container {
|
|
23
|
+
display: grid;
|
|
24
|
+
// grid-template-columns: 50px auto 50px;
|
|
25
|
+
|
|
26
|
+
&.scroll {
|
|
27
|
+
grid-template-columns: 50px auto 50px;
|
|
28
|
+
gap: 2px;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hide {
|
|
33
|
+
display: none;
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Button } from '../../../Buttons';
|
|
4
|
+
import { IconName } from '../../../Icons';
|
|
5
|
+
import classes from './ScrollContainer.scss';
|
|
6
|
+
import { useScroll } from './useScroll';
|
|
7
|
+
|
|
8
|
+
export const ScrollContainer: React.FC = ({ children }) => {
|
|
9
|
+
const {
|
|
10
|
+
scrollRef,
|
|
11
|
+
showScroll,
|
|
12
|
+
scrollLeft,
|
|
13
|
+
scrollRight,
|
|
14
|
+
enableScrollLeft,
|
|
15
|
+
enableScrollRight,
|
|
16
|
+
} = useScroll<HTMLDivElement>();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={clsx(classes.container, { [classes.scroll]: showScroll })}>
|
|
20
|
+
<Button
|
|
21
|
+
icon={IconName.ChevronLeft}
|
|
22
|
+
onButtonClicked={scrollLeft}
|
|
23
|
+
className={clsx({ [classes.hide]: !showScroll })}
|
|
24
|
+
disabled={!enableScrollLeft}
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<div className={classes.tablistWrapper} ref={scrollRef}>
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<Button
|
|
32
|
+
icon={IconName.ChevronRight}
|
|
33
|
+
onButtonClicked={scrollRight}
|
|
34
|
+
className={clsx({ [classes.hide]: !showScroll })}
|
|
35
|
+
disabled={!enableScrollRight}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScrollContainer } from './ScrollContainer';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import debounce from 'lodash/debounce';
|
|
2
|
+
import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
export const useScroll = <T extends HTMLElement>(): {
|
|
5
|
+
scrollLeft: () => void;
|
|
6
|
+
scrollRight: () => void;
|
|
7
|
+
scrollRef: React.RefObject<T>;
|
|
8
|
+
showScroll: boolean;
|
|
9
|
+
enableScrollLeft: boolean;
|
|
10
|
+
enableScrollRight: boolean;
|
|
11
|
+
} => {
|
|
12
|
+
const scrollRef = useRef<T>(null);
|
|
13
|
+
|
|
14
|
+
const scrollLeft = useCallback(() => {
|
|
15
|
+
if (scrollRef.current) {
|
|
16
|
+
scrollRef.current.scrollBy({
|
|
17
|
+
left: -220,
|
|
18
|
+
behavior: 'smooth',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const scrollRight = useCallback(() => {
|
|
24
|
+
if (scrollRef.current) {
|
|
25
|
+
scrollRef.current.scrollBy({
|
|
26
|
+
left: 220,
|
|
27
|
+
behavior: 'smooth',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const [showScroll, setShowScroll] = React.useState<boolean>(false);
|
|
33
|
+
const [enableScrollLeft, setEnableScrollLeft] =
|
|
34
|
+
React.useState<boolean>(false);
|
|
35
|
+
const [enableScrollRight, setEnableScrollRight] =
|
|
36
|
+
React.useState<boolean>(false);
|
|
37
|
+
|
|
38
|
+
const updateScroll = useCallback(() => {
|
|
39
|
+
if (scrollRef.current) {
|
|
40
|
+
const { scrollWidth, clientWidth } = scrollRef.current;
|
|
41
|
+
|
|
42
|
+
if (showScroll) {
|
|
43
|
+
// take into account the width of the scroll buttons
|
|
44
|
+
setShowScroll(scrollWidth > clientWidth + 100);
|
|
45
|
+
} else {
|
|
46
|
+
setShowScroll(scrollWidth > clientWidth);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [showScroll]);
|
|
50
|
+
|
|
51
|
+
const updateScrollButtons = useCallback(() => {
|
|
52
|
+
if (scrollRef.current) {
|
|
53
|
+
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current;
|
|
54
|
+
|
|
55
|
+
setEnableScrollLeft(scrollLeft > 0);
|
|
56
|
+
setEnableScrollRight(scrollLeft + clientWidth < scrollWidth);
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
// Omitting the dependency array here since this effect needs to run at every render
|
|
61
|
+
// to handle the case where the components are dynamically added/removed
|
|
62
|
+
useEffect(updateScroll);
|
|
63
|
+
useEffect(updateScrollButtons);
|
|
64
|
+
|
|
65
|
+
useLayoutEffect(() => {
|
|
66
|
+
// debounce is needed to avoid re-rendering everything on every resize event
|
|
67
|
+
const debouncedUpdateScroll = debounce(updateScroll, 50);
|
|
68
|
+
|
|
69
|
+
// add window resize event listener on mount
|
|
70
|
+
window.addEventListener('resize', debouncedUpdateScroll);
|
|
71
|
+
|
|
72
|
+
debouncedUpdateScroll();
|
|
73
|
+
|
|
74
|
+
const scrollContainer = scrollRef.current;
|
|
75
|
+
const debouncedUpdateScrollButtons = debounce(updateScrollButtons, 50);
|
|
76
|
+
|
|
77
|
+
scrollContainer?.addEventListener('scroll', debouncedUpdateScrollButtons);
|
|
78
|
+
|
|
79
|
+
updateScrollButtons();
|
|
80
|
+
|
|
81
|
+
const transformScroll = (event: WheelEvent): void => {
|
|
82
|
+
if (event.deltaY !== 0 && scrollContainer) {
|
|
83
|
+
scrollContainer.scrollLeft += event.deltaY;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
scrollContainer?.addEventListener('wheel', transformScroll, {
|
|
88
|
+
passive: false, // added for scrolling on Safari
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// clear all event listeners on unmount
|
|
92
|
+
return () => {
|
|
93
|
+
debouncedUpdateScroll.cancel();
|
|
94
|
+
window.removeEventListener('resize', debouncedUpdateScroll);
|
|
95
|
+
|
|
96
|
+
debouncedUpdateScrollButtons.cancel();
|
|
97
|
+
scrollContainer?.removeEventListener(
|
|
98
|
+
'scroll',
|
|
99
|
+
debouncedUpdateScrollButtons,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
scrollContainer?.removeEventListener('wheel', transformScroll);
|
|
103
|
+
};
|
|
104
|
+
}, [showScroll, updateScroll, updateScrollButtons]);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
scrollLeft,
|
|
108
|
+
scrollRight,
|
|
109
|
+
scrollRef,
|
|
110
|
+
showScroll,
|
|
111
|
+
enableScrollLeft,
|
|
112
|
+
enableScrollRight,
|
|
113
|
+
};
|
|
114
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CustomTabList as TabList } from './CustomTabList';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
ReactTabsFunctionComponent,
|
|
5
|
+
TabPanel,
|
|
6
|
+
TabPanelProps,
|
|
7
|
+
} from 'react-tabs';
|
|
8
|
+
import classes from './CustomTabPanel.scss';
|
|
9
|
+
|
|
10
|
+
export const CustomTabPanel: ReactTabsFunctionComponent<TabPanelProps> = ({
|
|
11
|
+
children,
|
|
12
|
+
className,
|
|
13
|
+
selectedClassName,
|
|
14
|
+
...otherProps
|
|
15
|
+
}) => (
|
|
16
|
+
<TabPanel
|
|
17
|
+
{...otherProps}
|
|
18
|
+
className={clsx(className, classes.tabpanel)}
|
|
19
|
+
selectedClassName={clsx(selectedClassName, classes.selected)}
|
|
20
|
+
data-test-id="tab"
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</TabPanel>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CustomTabPanel.tabsRole = 'TabPanel';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CustomTabPanel as TabPanel } from './CustomTabPanel';
|