@centreon/ui 26.12.4 → 26.12.6

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": "26.12.4",
3
+ "version": "26.12.6",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -110,3 +110,42 @@ export const exclusiveCheckboxGroup = (): JSX.Element => (
110
110
  validationSchema={object()}
111
111
  />
112
112
  );
113
+
114
+ export const segmentedButtons = (): JSX.Element => (
115
+ <Form
116
+ initialValues={{
117
+ activeChecks: 'use_default',
118
+ updateMode: 'incremental'
119
+ }}
120
+ inputs={[
121
+ {
122
+ fieldName: 'activeChecks',
123
+ group: '',
124
+ label: 'Active checks enabled',
125
+ segmentedButtons: {
126
+ options: [
127
+ { label: 'Yes', value: 'true' },
128
+ { label: 'No', value: 'false' },
129
+ { label: 'Default', value: 'use_default' }
130
+ ]
131
+ },
132
+ type: InputType.SegmentedButtons
133
+ },
134
+ {
135
+ fieldName: 'updateMode',
136
+ group: '',
137
+ label: 'Update mode (long labels)',
138
+ segmentedButtons: {
139
+ options: [
140
+ { label: 'Incremental', value: 'incremental' },
141
+ { label: 'Replacement', value: 'replacement' },
142
+ { label: 'Inherit from the host template', value: 'inherit' }
143
+ ]
144
+ },
145
+ type: InputType.SegmentedButtons
146
+ }
147
+ ]}
148
+ submit={submit}
149
+ validationSchema={object()}
150
+ />
151
+ );
@@ -27,38 +27,44 @@ const ValueDisplay = (): JSX.Element => {
27
27
  };
28
28
 
29
29
  interface InitializeProps {
30
+ columnWidth?: number;
30
31
  disabled?: boolean;
31
32
  enabled?: string;
33
+ segmentedOptions?: Array<{ label: string; value: string }>;
32
34
  }
33
35
 
34
36
  const initialize = ({
35
37
  enabled = 'use_default',
36
- disabled = false
38
+ disabled = false,
39
+ columnWidth = 800,
40
+ segmentedOptions = options
37
41
  }: InitializeProps = {}): void => {
38
42
  render(
39
- <Form
40
- initialValues={{ enabled }}
41
- inputs={[
42
- {
43
- dataTestId: 'enabled',
44
- fieldName: 'enabled',
45
- getDisabled: () => disabled,
46
- group: '',
47
- label: 'Notification Enabled',
48
- segmentedButtons: { options },
49
- type: InputType.SegmentedButtons
50
- },
51
- {
52
- custom: { Component: ValueDisplay },
53
- fieldName: 'value',
54
- group: '',
55
- label: 'value',
56
- type: InputType.Custom
57
- }
58
- ]}
59
- submit={rs.fn()}
60
- validationSchema={object()}
61
- />
43
+ <div style={{ width: columnWidth }}>
44
+ <Form
45
+ initialValues={{ enabled }}
46
+ inputs={[
47
+ {
48
+ dataTestId: 'enabled',
49
+ fieldName: 'enabled',
50
+ getDisabled: () => disabled,
51
+ group: '',
52
+ label: 'Notification Enabled',
53
+ segmentedButtons: { options: segmentedOptions },
54
+ type: InputType.SegmentedButtons
55
+ },
56
+ {
57
+ custom: { Component: ValueDisplay },
58
+ fieldName: 'value',
59
+ group: '',
60
+ label: 'value',
61
+ type: InputType.Custom
62
+ }
63
+ ]}
64
+ submit={rs.fn()}
65
+ validationSchema={object()}
66
+ />
67
+ </div>
62
68
  );
63
69
  };
64
70
 
@@ -104,6 +110,61 @@ describe('Segmented buttons', () => {
104
110
  expect(await button('No')).toHaveAttribute('aria-pressed', 'true');
105
111
  });
106
112
 
113
+ it('sizes to its labels, not to its column', async () => {
114
+ initialize({ columnWidth: 800 });
115
+
116
+ const group = await screen.findByTestId('enabled');
117
+ const widths = (
118
+ await Promise.all(['Yes', 'No', 'Default'].map(button))
119
+ ).map((element) => element.getBoundingClientRect().width);
120
+
121
+ // Three equal segments of at least 104px (26 spacing units), left aligned in the column
122
+ // (a pixel apart at most: the group overlaps the borders of its buttons).
123
+ expect(Math.max(...widths) - Math.min(...widths)).toBeLessThanOrEqual(1);
124
+ expect(Math.min(...widths)).toBeGreaterThanOrEqual(104);
125
+ expect(group.getBoundingClientRect().width).toBeLessThan(400);
126
+ expect(group.getBoundingClientRect().left).toBe(
127
+ (await screen.findByText('Notification Enabled')).getBoundingClientRect()
128
+ .left
129
+ );
130
+ });
131
+
132
+ it('stays inside a column narrower than its segments', async () => {
133
+ initialize({ columnWidth: 200 });
134
+
135
+ const group = await screen.findByTestId('enabled');
136
+ const wrapper = group.parentElement as HTMLElement;
137
+
138
+ expect(wrapper.getBoundingClientRect().width).toBeLessThanOrEqual(200);
139
+ expect(wrapper.scrollWidth).toBeGreaterThan(wrapper.clientWidth);
140
+ expect((await button('Default')).scrollWidth).toBeLessThanOrEqual(
141
+ (await button('Default')).clientWidth
142
+ );
143
+ });
144
+
145
+ it('widens every segment to the longest label, without wrapping', async () => {
146
+ const longLabel = 'Inherit from the host template';
147
+
148
+ initialize({
149
+ segmentedOptions: [
150
+ { label: 'Yes', value: 'true' },
151
+ { label: 'No', value: 'false' },
152
+ { label: longLabel, value: 'use_default' }
153
+ ]
154
+ });
155
+
156
+ const yes = await button('Yes');
157
+ const long = await button(longLabel);
158
+
159
+ expect(
160
+ Math.abs(
161
+ yes.getBoundingClientRect().width - long.getBoundingClientRect().width
162
+ )
163
+ ).toBeLessThanOrEqual(1);
164
+ expect(long.scrollWidth).toBeLessThanOrEqual(long.clientWidth);
165
+ expect(long.getBoundingClientRect().height).toBe(28);
166
+ });
167
+
107
168
  it('does not change when disabled', async () => {
108
169
  initialize({ disabled: true });
109
170
 
@@ -33,35 +33,39 @@ const SegmentedButtons = ({
33
33
  Component: (
34
34
  <FormGroup>
35
35
  <FormLabel>{t(label)}</FormLabel>
36
- <ButtonGroup
37
- aria-label={t(label)}
38
- // As wide as its column, up to what three labels need.
39
- className="max-w-[412px]"
40
- color="primary"
41
- data-testid={dataTestId}
42
- disabled={disabled}
43
- fullWidth
44
- size="small"
45
- >
46
- {segmentedButtons?.options.map(
47
- ({ label: optionLabel, value: optionValue }) => {
48
- const isSelected = equals(value, optionValue);
36
+ {/* In a column narrower than the segments, the group scrolls inside it
37
+ rather than overflowing into what is next to it. */}
38
+ <div className="max-w-full self-start overflow-x-auto">
39
+ <ButtonGroup
40
+ aria-label={t(label)}
41
+ // Equal segments, each as wide as the longest label (26 spacing
42
+ // units, 104px, at least), whatever the width of the column.
43
+ className="inline-grid auto-cols-[minmax(calc(var(--spacing)*26),1fr)] grid-flow-col"
44
+ color="primary"
45
+ data-testid={dataTestId}
46
+ disabled={disabled}
47
+ size="small"
48
+ >
49
+ {segmentedButtons?.options.map(
50
+ ({ label: optionLabel, value: optionValue }) => {
51
+ const isSelected = equals(value, optionValue);
49
52
 
50
- return (
51
- <Button
52
- aria-pressed={isSelected}
53
- className="h-7 px-3 py-0 font-medium text-sm leading-[21px] normal-case"
54
- data-testid={`${dataTestId}-${optionValue}`}
55
- key={optionValue}
56
- onClick={select(optionValue)}
57
- variant={isSelected ? 'contained' : 'outlined'}
58
- >
59
- {t(optionLabel)}
60
- </Button>
61
- );
62
- }
63
- )}
64
- </ButtonGroup>
53
+ return (
54
+ <Button
55
+ aria-pressed={isSelected}
56
+ className="h-7 whitespace-nowrap px-3 py-0 font-medium text-sm leading-[21px] normal-case"
57
+ data-testid={`${dataTestId}-${optionValue}`}
58
+ key={optionValue}
59
+ onClick={select(optionValue)}
60
+ variant={isSelected ? 'contained' : 'outlined'}
61
+ >
62
+ {t(optionLabel)}
63
+ </Button>
64
+ );
65
+ }
66
+ )}
67
+ </ButtonGroup>
68
+ </div>
65
69
  </FormGroup>
66
70
  ),
67
71
  memoProps: [value, disabled, segmentedButtons, additionalMemoProps]
@@ -51,6 +51,15 @@ export const WithDescription: Story = {
51
51
  render: Default.render
52
52
  };
53
53
 
54
+ export const WithInlineDescription: Story = {
55
+ args: {
56
+ description: 'Configure the hosts monitored by your platform',
57
+ descriptionPosition: 'inline',
58
+ title: 'Hosts'
59
+ },
60
+ render: Default.render
61
+ };
62
+
54
63
  export const WithMenu: Story = {
55
64
  args: {
56
65
  ...WithDescription.args,
@@ -7,9 +7,11 @@ type PageHeaderMainProps = {
7
7
  };
8
8
 
9
9
  const PageHeaderMain = ({ children }: PageHeaderMainProps): ReactElement => {
10
- const { classes } = useStyles();
10
+ const { classes, cx } = useStyles();
11
11
 
12
- return <div className={classes.pageHeaderMain}>{children}</div>;
12
+ return (
13
+ <div className={cx(classes.pageHeaderMain, 'min-w-0')}>{children}</div>
14
+ );
13
15
  };
14
16
 
15
17
  export { PageHeaderMain };
@@ -8,37 +8,80 @@ import { useStyles } from './PageHeader.styles';
8
8
  type PageHeaderTitleProps = {
9
9
  actions?: ReactNode;
10
10
  description?: string;
11
+ descriptionPosition?: 'below' | 'inline';
11
12
  title: string;
12
13
  };
13
14
 
14
15
  const PageHeaderTitle = ({
15
16
  actions,
16
17
  title,
17
- description
18
+ description,
19
+ descriptionPosition = 'below'
18
20
  }: PageHeaderTitleProps): ReactElement => {
19
21
  const { classes } = useStyles();
20
22
 
23
+ const isInline = descriptionPosition === 'inline';
24
+
25
+ const descriptionElement = description && (
26
+ <TextOverflowTooltip label={description}>
27
+ <MuiTypography
28
+ aria-label="page header description"
29
+ className={classes.pageHeaderTitleDescription}
30
+ variant="body2"
31
+ >
32
+ {description}
33
+ </MuiTypography>
34
+ </TextOverflowTooltip>
35
+ );
36
+
37
+ // Inline, the title owns its typography: the `pageHeaderTitle` rules that
38
+ // style the `h1` of the stacked layout only apply to that layout.
39
+ const titleElement = isInline ? (
40
+ <MuiTypography
41
+ aria-label="page header title"
42
+ className="m-0 shrink-0 font-bold leading-none"
43
+ component="h1"
44
+ variant="h5"
45
+ >
46
+ {title}
47
+ </MuiTypography>
48
+ ) : (
49
+ <MuiTypography aria-label="page header title" variant="h1">
50
+ {title}
51
+ </MuiTypography>
52
+ );
53
+
54
+ const actionsElement = actions && (
55
+ <div className={classes.pageHeaderTitleActions}>{actions}</div>
56
+ );
57
+
58
+ if (isInline) {
59
+ return (
60
+ <div className="flex min-w-0 items-baseline gap-4 self-start">
61
+ {titleElement}
62
+ {description && (
63
+ <>
64
+ <span
65
+ aria-hidden
66
+ className="shrink-0 text-2xl font-light leading-none text-header-page-description"
67
+ >
68
+ |
69
+ </span>
70
+ <div className="min-w-0">{descriptionElement}</div>
71
+ </>
72
+ )}
73
+ {actionsElement}
74
+ </div>
75
+ );
76
+ }
77
+
21
78
  return (
22
79
  <div className={classes.pageHeaderTitle}>
23
80
  <span>
24
- <MuiTypography aria-label="page header title" variant="h1">
25
- {title}
26
- </MuiTypography>
27
- {actions && (
28
- <div className={classes.pageHeaderTitleActions}>{actions}</div>
29
- )}
81
+ {titleElement}
82
+ {actionsElement}
30
83
  </span>
31
- {description && (
32
- <TextOverflowTooltip label={description}>
33
- <MuiTypography
34
- aria-label="page header description"
35
- className={classes.pageHeaderTitleDescription}
36
- variant="body2"
37
- >
38
- {description}
39
- </MuiTypography>
40
- </TextOverflowTooltip>
41
- )}
84
+ {descriptionElement}
42
85
  </div>
43
86
  );
44
87
  };