@centreon/ui 25.7.3 → 25.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "25.7.3",
3
+ "version": "25.8.1",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -35,12 +35,6 @@ const useStyles = makeStyles()((theme) => ({
35
35
  display: 'flex',
36
36
  flexDirection: 'row'
37
37
  },
38
- groupTitleIcon: {
39
- alignItems: 'center',
40
- columnGap: theme.spacing(1),
41
- display: 'flex',
42
- flexDirection: 'row'
43
- },
44
38
  tooltip: {
45
39
  maxWidth: theme.spacing(60)
46
40
  },
@@ -77,7 +71,7 @@ const CollapsibleGroup = ({
77
71
  disableGutters
78
72
  disableRipple
79
73
  aria-label={group?.name}
80
- className={cx(classes.groupTitleContainer, containerClassName)}
74
+ className={`${cx(classes.groupTitleContainer, containerClassName)} bg-background-listing-header`}
81
75
  onClick={toggle}
82
76
  >
83
77
  {containerComponentChildren}
@@ -94,12 +88,17 @@ const CollapsibleGroup = ({
94
88
  <>
95
89
  {hasGroupTitle && (
96
90
  <ContainerComponent>
97
- {isCollapsible && <CollapseIcon />}
98
- <div className={classes.groupTitleIcon}>
91
+ <div
92
+ data-testid={`${group?.name}-header`}
93
+ className={
94
+ 'snap-y flex flex-row justify-between w-full pl-3 pr-1 text-white items-center'
95
+ }
96
+ >
99
97
  <Typography
100
- className="groupText"
98
+ className="groupText scroll-m-12 snap-start"
101
99
  variant="h6"
102
100
  {...group?.titleAttributes}
101
+ data-section-group-form-id={group?.name}
103
102
  >
104
103
  {t(group?.name as string)}
105
104
  </Typography>
@@ -116,6 +115,7 @@ const CollapsibleGroup = ({
116
115
  </MuiIconButton>
117
116
  </Tooltip>
118
117
  )}
118
+ {isCollapsible && <CollapseIcon />}
119
119
  </div>
120
120
  </ContainerComponent>
121
121
  )}
@@ -132,16 +132,129 @@ describe('Form list', () => {
132
132
  });
133
133
  });
134
134
 
135
- const initializeFile = (): void => {
135
+ const initializeFormWithSections = (): void => {
136
136
  cy.mount({
137
137
  Component: (
138
138
  <Form
139
+ isCollapsible
139
140
  initialValues={{
140
141
  list: []
141
142
  }}
143
+ groups={[
144
+ {
145
+ name: 'First group',
146
+ order: 1
147
+ },
148
+ {
149
+ name: 'Third group',
150
+ order: 3
151
+ },
152
+ {
153
+ name: 'Second group',
154
+ order: 2
155
+ },
156
+ {
157
+ name: 'Fourth group',
158
+ order: 4
159
+ }
160
+ ]}
142
161
  inputs={[
143
162
  {
144
- fieldName: 'file',
163
+ fieldName: 'First name',
164
+ group: 'First group',
165
+ label: 'Name',
166
+ type: InputType.Text
167
+ },
168
+ {
169
+ fieldName: 'Divider',
170
+ group: 'First group',
171
+ label: 'Divider',
172
+ type: InputType.Divider
173
+ },
174
+ {
175
+ fieldName: 'Second name',
176
+ group: 'First group',
177
+ label: 'Name',
178
+ type: InputType.Text
179
+ },
180
+ {
181
+ fieldName: 'Third name',
182
+ group: 'First group',
183
+ label: 'Name',
184
+ type: InputType.Text
185
+ },
186
+ {
187
+ fieldName: 'Fourth name',
188
+ group: 'First group',
189
+ label: 'Name',
190
+ type: InputType.Text
191
+ },
192
+ {
193
+ fieldName: 'Fifth name',
194
+ group: 'First group',
195
+ label: 'Name',
196
+ type: InputType.Text
197
+ },
198
+ {
199
+ fieldName: 'Sixth name',
200
+ group: 'First group',
201
+ label: 'Name',
202
+ type: InputType.Text
203
+ },
204
+ {
205
+ fieldName: 'Seventh name',
206
+ group: 'First group',
207
+ label: 'Name',
208
+ type: InputType.Text
209
+ },
210
+ {
211
+ fieldName: 'Eighth name',
212
+ group: 'First group',
213
+ label: 'Name',
214
+ type: InputType.Text
215
+ },
216
+ {
217
+ fieldName: 'Ninth name',
218
+ group: 'First group',
219
+ label: 'Name',
220
+ type: InputType.Text
221
+ },
222
+ {
223
+ fieldName: 'First second group name',
224
+ group: 'Second group',
225
+ label: 'Name',
226
+ type: InputType.Text
227
+ },
228
+ {
229
+ fieldName: 'First third group name',
230
+ group: 'Third group',
231
+ label: 'Name',
232
+ type: InputType.Text
233
+ },
234
+ {
235
+ fieldName: 'First fourth group name',
236
+ group: 'Fourth group',
237
+ label: 'Name',
238
+ type: InputType.Text
239
+ }
240
+ ]}
241
+ submit={cy.stub()}
242
+ validationSchema={object()}
243
+ />
244
+ )
245
+ });
246
+ };
247
+
248
+ const initializeFile = () => {
249
+ cy.mount({
250
+ Component: (
251
+ <Form
252
+ initialValues={{
253
+ list: []
254
+ }}
255
+ inputs={[
256
+ {
257
+ fieldName: 'list',
145
258
  group: '',
146
259
  label: 'json',
147
260
  type: InputType.File,
@@ -170,3 +283,25 @@ describe('File', () => {
170
283
  cy.makeSnapshot();
171
284
  });
172
285
  });
286
+
287
+ describe('Form with sections', () => {
288
+ beforeEach(initializeFormWithSections);
289
+ it('displays sections when correct amount of sections', () => {
290
+ cy.contains('First group').should('be.visible');
291
+ cy.contains('Second group').should('be.visible');
292
+ cy.contains('Third group').should('be.visible');
293
+ cy.contains('Fourth group').should('be.visible');
294
+ cy.makeSnapshot();
295
+ });
296
+
297
+ it('scrolls correctly to section', () => {
298
+ cy.window().then((win) => {
299
+ const initialScrollY = win.scrollY;
300
+ cy.contains('Third group').click();
301
+ cy.window().its('scrollY').should('be.greaterThan', initialScrollY);
302
+ });
303
+
304
+ cy.wait(500); // Wait for the scroll animation to complete
305
+ cy.makeSnapshot();
306
+ });
307
+ });
@@ -1,5 +1,3 @@
1
- import { Paper } from '@mui/material';
2
-
3
1
  import { Form, GroupDirection } from './Form';
4
2
  import {
5
3
  BasicForm,
@@ -27,51 +25,33 @@ const mandatoryProps = {
27
25
  };
28
26
 
29
27
  export const basicForm = (): JSX.Element => (
30
- <Paper elevation={0} sx={{ p: 1 }}>
31
- <Form<BasicForm> {...mandatoryProps} />
32
- </Paper>
28
+ <Form<BasicForm> {...mandatoryProps} />
33
29
  );
34
30
 
35
31
  export const basicFormWithGroups = (): JSX.Element => (
36
- <Paper elevation={0} sx={{ p: 1 }}>
37
- <Form<BasicForm> {...mandatoryProps} groups={basicFormGroups} />
38
- </Paper>
32
+ <Form<BasicForm> {...mandatoryProps} groups={basicFormGroups} />
39
33
  );
40
34
 
41
35
  export const basicFormWithCollapsibleGroups = (): JSX.Element => (
42
- <Paper elevation={0} sx={{ p: 1 }}>
43
- <Form<BasicForm>
44
- {...mandatoryProps}
45
- isCollapsible
46
- groups={basicFormGroups}
47
- />
48
- </Paper>
36
+ <Form<BasicForm> {...mandatoryProps} isCollapsible groups={basicFormGroups} />
49
37
  );
50
38
 
51
39
  export const basicFormWithCustomButton = (): JSX.Element => (
52
- <Paper elevation={0} sx={{ p: 1 }}>
53
- <Form<BasicForm> {...mandatoryProps} Buttons={CustomButton} />
54
- </Paper>
40
+ <Form<BasicForm> {...mandatoryProps} Buttons={CustomButton} />
55
41
  );
56
42
 
57
43
  export const loadingForm = (): JSX.Element => (
58
- <Paper elevation={0} sx={{ p: 1 }}>
59
- <Form<BasicForm> {...mandatoryProps} isLoading />
60
- </Paper>
44
+ <Form<BasicForm> {...mandatoryProps} isLoading />
61
45
  );
62
46
 
63
47
  export const loadingFormWithGroups = (): JSX.Element => (
64
- <Paper elevation={0} sx={{ p: 1 }}>
65
- <Form<BasicForm> {...mandatoryProps} isLoading groups={basicFormGroups} />
66
- </Paper>
48
+ <Form<BasicForm> {...mandatoryProps} isLoading groups={basicFormGroups} />
67
49
  );
68
50
 
69
51
  export const basicFormWithHorizontalGroups = (): JSX.Element => (
70
- <Paper elevation={0} sx={{ p: 1 }}>
71
- <Form<BasicForm>
72
- {...mandatoryProps}
73
- groupDirection={GroupDirection.Horizontal}
74
- groups={basicFormGroups.filter((group) => group.order !== 3)}
75
- />
76
- </Paper>
52
+ <Form<BasicForm>
53
+ {...mandatoryProps}
54
+ groupDirection={GroupDirection.Horizontal}
55
+ groups={basicFormGroups.filter((group) => group.order !== 3)}
56
+ />
77
57
  );
package/src/Form/Form.tsx CHANGED
@@ -11,6 +11,7 @@ import { useStyles } from './Form.styles';
11
11
  import FormButtons from './FormButtons';
12
12
  import Inputs from './Inputs';
13
13
  import { Group, InputProps } from './Inputs/models';
14
+ import { FormSection } from './Section/FormSection';
14
15
 
15
16
  export enum GroupDirection {
16
17
  Horizontal = 'horizontal',
@@ -76,6 +77,7 @@ const Form = <T extends object>({
76
77
  {...formikSharedConfig}
77
78
  >
78
79
  <div>
80
+ <FormSection groups={groups} />
79
81
  {children}
80
82
  <div className={cx(className, classes.form)}>
81
83
  <Inputs
@@ -1,5 +1,3 @@
1
- import { makeStyles } from 'tss-react/mui';
2
-
3
1
  import { InputPropsWithoutGroup } from './models';
4
2
 
5
3
  import { Box, Typography } from '@mui/material';
@@ -7,34 +5,10 @@ import { FormikValues, useFormikContext } from 'formik';
7
5
  import { isNotEmpty, isNotNil } from 'ramda';
8
6
  import { getInput } from '.';
9
7
 
10
- interface StylesProps {
11
- alignItems?: string;
12
- columns?: number;
13
- gridTemplateColumns?: string;
14
- }
15
-
16
- const useStyles = makeStyles<StylesProps>()(
17
- (theme, { columns, gridTemplateColumns, alignItems }) => ({
18
- gridFields: {
19
- alignItems: alignItems || 'flex-start',
20
- columnGap: theme.spacing(4),
21
- display: 'grid',
22
- gridTemplateColumns: gridTemplateColumns || `repeat(${columns}, 1fr)`,
23
- rowGap: theme.spacing(2)
24
- }
25
- })
26
- );
27
-
28
8
  const Grid = ({
29
9
  grid,
30
10
  hideInput
31
11
  }: InputPropsWithoutGroup): JSX.Element | null => {
32
- const { classes, cx } = useStyles({
33
- alignItems: grid?.alignItems,
34
- columns: grid?.columns.length,
35
- gridTemplateColumns: grid?.gridTemplateColumns
36
- });
37
-
38
12
  const { values } = useFormikContext<FormikValues>();
39
13
 
40
14
  if (hideInput?.(values) ?? false) {
@@ -44,7 +18,15 @@ const Grid = ({
44
18
  const className = grid?.className || '';
45
19
 
46
20
  return (
47
- <div className={cx(classes.gridFields, className)}>
21
+ <div
22
+ className={`${className} grid gap-3`}
23
+ style={{
24
+ gridTemplateColumns:
25
+ className ? grid?.gridTemplateColumns || undefined : grid?.gridTemplateColumns ||
26
+ `repeat(${grid?.columns.length || 1}, 1fr)`,
27
+ alignItems: grid?.alignItems || 'flex-start'
28
+ }}
29
+ >
48
30
  {grid?.columns.map((field) => {
49
31
  const Input = getInput(field.type);
50
32
 
@@ -62,7 +44,7 @@ const Grid = ({
62
44
  {field.additionalLabel && (
63
45
  <Typography
64
46
  sx={{ marginBottom: 0.5, color: 'primary.main' }}
65
- className={cx(field?.additionalLabelClassName)}
47
+ className={field?.additionalLabelClassName}
66
48
  variant="h6"
67
49
  >
68
50
  {field.additionalLabel}
@@ -0,0 +1,7 @@
1
+ import Divider from '@mui/material/Divider';
2
+
3
+ const SubgroupDivider = () => {
4
+ return <Divider className="border-dashed" />;
5
+ };
6
+
7
+ export { SubgroupDivider };
@@ -127,6 +127,7 @@ const Text = ({
127
127
  ariaLabel={t(label) || ''}
128
128
  autoFocus={autoFocus}
129
129
  dataTestId={dataTestId || label}
130
+ data-testid-suffix={`test-${label}`}
130
131
  disabled={disabled}
131
132
  error={error as string | undefined}
132
133
  label={t(label)}
@@ -43,6 +43,7 @@ import Grid from './Grid';
43
43
  import List from './List/List';
44
44
  import LoadingSkeleton from './LoadingSkeleton';
45
45
  import RadioInput from './Radio';
46
+ import { SubgroupDivider } from './SubGroupDivider';
46
47
  import SwitchInput from './Switch';
47
48
  import TextInput from './Text';
48
49
  import { Group, InputProps, InputPropsWithoutGroup, InputType } from './models';
@@ -82,6 +83,11 @@ export const getInput = cond<
82
83
  ],
83
84
  [equals(InputType.List) as (b: InputType) => boolean, always(List)],
84
85
  [equals(InputType.File) as (b: InputType) => boolean, always(File)],
86
+ [equals(InputType.Text) as (b: InputType) => boolean, always(TextInput)],
87
+ [
88
+ equals(InputType.Divider) as (b: InputType) => boolean,
89
+ always(SubgroupDivider)
90
+ ],
85
91
  [T, always(TextInput)]
86
92
  ]);
87
93
 
@@ -21,7 +21,8 @@ export enum InputType {
21
21
  Checkbox = 11,
22
22
  CheckboxGroup = 12,
23
23
  List = 13,
24
- File = 14
24
+ File = 14,
25
+ Divider = 15
25
26
  }
26
27
 
27
28
  interface FieldsTableGetRequiredProps {
@@ -0,0 +1,34 @@
1
+ import { Box, TabsProps } from '@mui/material';
2
+ import { isNil } from 'ramda';
3
+ import { useMemo } from 'react';
4
+ import { Tabs } from '../../components/Tabs';
5
+ import { Group } from '../Inputs/models';
6
+ import { groupToTab } from './PanelTabs';
7
+ import { useNavigateToSection } from './navigateToSection';
8
+
9
+ export interface FormSectionProps extends TabsProps {
10
+ groups?: Group[];
11
+ }
12
+
13
+ const FormSection = ({ groups }: FormSectionProps) => {
14
+ if (isNil(groups) || groups.length < 4) {
15
+ return null;
16
+ }
17
+
18
+ const navigateToSection = useNavigateToSection();
19
+ const tabMemo = useMemo(() => groupToTab(groups), [groups]);
20
+
21
+ return (
22
+ <Box className="sticky top-0 bg-background-paper z-100">
23
+ <Tabs
24
+ variant="scrollable"
25
+ scrollButtons={false}
26
+ tabs={tabMemo}
27
+ defaultTab={tabMemo[0].value}
28
+ onChange={navigateToSection}
29
+ />
30
+ </Box>
31
+ );
32
+ };
33
+
34
+ export { FormSection };
@@ -0,0 +1,13 @@
1
+ import { prop, sortBy } from 'ramda';
2
+ import { TabI } from 'src/components/Tabs/Tabs';
3
+ import { Group } from '../Inputs/models';
4
+
5
+ const groupToTab = (groups: Array<Group>): Array<TabI> => {
6
+ const sortedGroups = sortBy(prop('order'), groups);
7
+
8
+ return sortedGroups.map((group) => {
9
+ return { value: group.name, label: group.name };
10
+ });
11
+ };
12
+
13
+ export { groupToTab };
@@ -0,0 +1,9 @@
1
+ export const useNavigateToSection = () => {
2
+ return (sectionName: string) => {
3
+ const section = document.querySelector(
4
+ `[data-section-group-form-id="${sectionName}"]`
5
+ );
6
+
7
+ section?.scrollIntoView({ behavior: 'smooth' });
8
+ };
9
+ };
@@ -157,12 +157,16 @@ export const basicFormGroups: Array<Group> = [
157
157
  {
158
158
  EndIcon: () => <HelpOutlineIcon />,
159
159
  TooltipContent: (): JSX.Element => <Typography>Tooltip content</Typography>,
160
+ name: 'Third group',
161
+ order: 3
162
+ },
163
+ {
160
164
  name: 'Second group',
161
165
  order: 2
162
166
  },
163
167
  {
164
- name: 'Third group',
165
- order: 3
168
+ name: 'Fourth group',
169
+ order: 4
166
170
  }
167
171
  ];
168
172
 
@@ -215,6 +219,12 @@ export const basicFormInputs: Array<InputProps> = [
215
219
  },
216
220
  type: InputType.Radio
217
221
  },
222
+ {
223
+ fieldName: 'div',
224
+ group: 'First group',
225
+ label: 'divider',
226
+ type: InputType.Divider
227
+ },
218
228
  {
219
229
  additionalLabel: 'Notifications',
220
230
  fieldName: '',
@@ -4,6 +4,7 @@ import { Button } from '../Button';
4
4
 
5
5
  import { Modal } from '.';
6
6
  import '../../ThemeProvider/tailwindcss.css';
7
+ import { basicFormWithCollapsibleGroups } from '../../Form/Form.stories';
7
8
 
8
9
  const meta: Meta<typeof Modal> = {
9
10
  argTypes: {
@@ -45,6 +46,25 @@ export const Default: Story = {
45
46
  )
46
47
  };
47
48
 
49
+ export const WithForm: Story = {
50
+ args: {
51
+ ...Default.args
52
+ },
53
+
54
+ render: (args) => (
55
+ <Modal {...args}>
56
+ <Modal.Header>Modal title</Modal.Header>
57
+ <Modal.Body>{basicFormWithCollapsibleGroups()}</Modal.Body>
58
+ <Modal.Actions
59
+ labels={{
60
+ cancel: 'Cancel',
61
+ confirm: 'Confirm'
62
+ }}
63
+ />
64
+ </Modal>
65
+ )
66
+ };
67
+
48
68
  export const AsDangerAction: Story = {
49
69
  args: {
50
70
  ...Default.args
@@ -8,7 +8,7 @@ const useStyles = makeStyles<{
8
8
  }>()((theme, props) => ({
9
9
  modal: {
10
10
  '& .MuiDialog-paper': {
11
- gap: theme.spacing(2),
11
+ gap: theme.spacing(3),
12
12
  padding: theme.spacing(2.5)
13
13
  },
14
14
  '&[data-size="fullscreen"]': {
@@ -65,15 +65,6 @@ const useStyles = makeStyles<{
65
65
  right: 0,
66
66
  zIndex: theme.zIndex.modal
67
67
  },
68
- modalBody: {
69
- '& > p': {
70
- '&:first-of-type': {
71
- margin: theme.spacing(0, 0, 1, 0)
72
- },
73
- margin: theme.spacing(1, 0, 1, 0),
74
- width: '90%'
75
- }
76
- },
77
68
  modalCloseButton: {
78
69
  position: 'absolute',
79
70
  right: theme.spacing(1),
@@ -82,19 +73,7 @@ const useStyles = makeStyles<{
82
73
  },
83
74
  top: theme.spacing(1)
84
75
  },
85
- modalHeader: {
86
- '& .MuiDialogTitle-root': {
87
- padding: theme.spacing(0)
88
- },
89
- display: 'flex',
90
- gap: theme.spacing(2),
91
-
92
- justifyContent: 'space-between'
93
- },
94
- modalTitle: {
95
- fontSize: theme.typography.h5.fontSize,
96
- fontWeight: theme.typography.fontWeightBold
97
- }
76
+
98
77
  }));
99
78
 
100
79
  export { useStyles };
@@ -54,7 +54,7 @@ const Modal = ({
54
54
  TransitionProps={{
55
55
  direction: 'up'
56
56
  }}
57
- className={classes.modal}
57
+ className={`${classes.modal} gap-6`}
58
58
  data-size={size}
59
59
  open={open}
60
60
  onClose={onClose}
@@ -1,15 +1,17 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
2
 
3
- import { useStyles } from './Modal.styles';
3
+ import { modalBody } from './modal.module.css';
4
4
 
5
5
  export type ModalHeaderProps = {
6
6
  children?: ReactNode;
7
7
  };
8
8
 
9
9
  const ModalBody = ({ children }: ModalHeaderProps): ReactElement => {
10
- const { classes } = useStyles();
11
-
12
- return <div className={classes.modalBody}>{children}</div>;
10
+ return (
11
+ <div className={modalBody} data-testid="modal-body">
12
+ {children}
13
+ </div>
14
+ );
13
15
  };
14
16
 
15
17
  export { ModalBody };
@@ -2,7 +2,9 @@ import { ReactElement, ReactNode } from 'react';
2
2
 
3
3
  import { DialogTitleProps, DialogTitle as MuiDialogTitle } from '@mui/material';
4
4
 
5
- import { useStyles } from './Modal.styles';
5
+ import '../../../src/ThemeProvider/tailwindcss.css';
6
+
7
+ import { modalHeader } from './modal.module.css';
6
8
 
7
9
  export type ModalHeaderProps = {
8
10
  children?: ReactNode;
@@ -12,11 +14,9 @@ const ModalHeader = ({
12
14
  children,
13
15
  ...rest
14
16
  }: ModalHeaderProps & DialogTitleProps): ReactElement => {
15
- const { classes } = useStyles();
16
-
17
17
  return (
18
- <div className={classes.modalHeader}>
19
- <MuiDialogTitle color="primary" className={classes.modalTitle} {...rest}>
18
+ <div className={modalHeader}>
19
+ <MuiDialogTitle className="p-0 font-bold text-2xl" color="primary" {...rest}>
20
20
  {children}
21
21
  </MuiDialogTitle>
22
22
  </div>
@@ -0,0 +1,16 @@
1
+ .modalHeader {
2
+ & .MuiDialogTitle-root {
3
+ padding: 0;
4
+ }
5
+
6
+ display: flex;
7
+ gap: var(--spacing-4);
8
+ justify-content: space-between;
9
+ }
10
+
11
+ .modalBody {
12
+ overflow-y: auto;
13
+ overflow-x: hidden;
14
+ height: 100%;
15
+ padding-right: var(--spacing-4); /* To prevent scrollbar from overlapping content */
16
+ }
@@ -5,12 +5,6 @@ export const useTabsStyles = makeStyles()((theme) => ({
5
5
  bottom: 'unset'
6
6
  },
7
7
  tab: {
8
- '&[aria-selected="true"]': {
9
- color: theme.palette.text.primary,
10
- fontWeight: theme.typography.fontWeightBold
11
- },
12
- color: theme.palette.text.primary,
13
- fontWeight: theme.typography.fontWeightRegular,
14
8
  marginRight: theme.spacing(2),
15
9
  minHeight: 0,
16
10
  minWidth: 0,
@@ -5,33 +5,53 @@ import { Tabs as MuiTabs, Tab, TabsProps } from '@mui/material';
5
5
 
6
6
  import { useTabsStyles } from './Tab.styles';
7
7
 
8
+ import '../../ThemeProvider/tailwindcss.css';
9
+
10
+ export interface TabI {
11
+ label: string;
12
+ value: string;
13
+ }
14
+
8
15
  type Props = {
9
- children: Array<JSX.Element>;
16
+ children?: Array<JSX.Element>;
10
17
  defaultTab: string;
11
18
  tabList?: TabsProps;
12
- tabs: Array<{
13
- label: string;
14
- value: string;
15
- }>;
19
+ variant?: 'standard' | 'scrollable' | 'fullWidth';
20
+ scrollButtons?: boolean | 'auto';
21
+ tabs: TabI[];
22
+ onChange?: (newValue: string) => void;
16
23
  };
17
24
 
18
25
  export const Tabs = ({
19
26
  children,
20
27
  defaultTab,
21
28
  tabs,
22
- tabList
29
+ tabList,
30
+ variant,
31
+ scrollButtons = 'auto',
32
+ onChange
23
33
  }: Props): JSX.Element => {
24
34
  const { classes } = useTabsStyles();
25
35
 
26
36
  const [selectedTab, setSelectedTab] = useState(defaultTab);
27
37
 
28
- const changeTab = useCallback((_, newValue: string): void => {
29
- setSelectedTab(newValue);
30
- }, []);
38
+ const changeTab = useCallback(
39
+ (_, newValue: string): void => {
40
+ if (onChange) onChange(newValue);
41
+
42
+ setSelectedTab(newValue);
43
+ },
44
+ [onChange]
45
+ );
46
+
47
+ const selectedTabStyle = ' font-bold text-primary-main';
48
+ const defaultTabStyle = ' font-normal';
31
49
 
32
50
  return (
33
51
  <TabContext value={selectedTab}>
34
52
  <MuiTabs
53
+ scrollButtons={scrollButtons}
54
+ variant={variant}
35
55
  classes={{
36
56
  indicator: classes.indicator,
37
57
  root: classes.tabs
@@ -43,10 +63,11 @@ export const Tabs = ({
43
63
  {tabs.map(({ value, label }) => (
44
64
  <Tab
45
65
  aria-label={label}
46
- className={classes.tab}
66
+ className={`${classes.tab} ${selectedTab === value ? selectedTabStyle : defaultTabStyle}`}
47
67
  key={value}
48
68
  label={label}
49
69
  value={value}
70
+ data-testid={`tab-${value}`}
50
71
  />
51
72
  ))}
52
73
  </MuiTabs>
package/src/index.ts CHANGED
@@ -89,6 +89,7 @@ export type {
89
89
  SearchMatch
90
90
  } from './api/buildListingEndpoint/models';
91
91
  export { default as buildListingDecoder } from './api/buildListingDecoder';
92
+ export { customFetch } from './api/customFetch';
92
93
 
93
94
  export { default as ContentWithCircularLoading } from './ContentWithCircularProgress';
94
95
  export {