@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 +1 -1
- package/src/Form/Form.stories.tsx +39 -0
- package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +85 -24
- package/src/Form/Inputs/SegmentedButtons.tsx +32 -28
- package/src/components/Header/PageHeader/PageHeader.stories.tsx +9 -0
- package/src/components/Header/PageHeader/PageHeaderMain.tsx +4 -2
- package/src/components/Header/PageHeader/PageHeaderTitle.tsx +61 -18
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
</MuiTypography>
|
|
27
|
-
{actions && (
|
|
28
|
-
<div className={classes.pageHeaderTitleActions}>{actions}</div>
|
|
29
|
-
)}
|
|
81
|
+
{titleElement}
|
|
82
|
+
{actionsElement}
|
|
30
83
|
</span>
|
|
31
|
-
{
|
|
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
|
};
|