@centreon/ui 26.12.6 → 26.12.7
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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { rs } from '@rstest/core';
|
|
2
|
+
import { render } from '@test-setup/render';
|
|
3
|
+
import { screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { object } from 'yup';
|
|
6
|
+
|
|
7
|
+
import { Form } from '../Form';
|
|
8
|
+
import { InputType } from './models';
|
|
9
|
+
|
|
10
|
+
const initialize = (): void => {
|
|
11
|
+
render(
|
|
12
|
+
<Form
|
|
13
|
+
initialValues={{ community: '', note: '' }}
|
|
14
|
+
inputs={[
|
|
15
|
+
{
|
|
16
|
+
dataTestId: 'note',
|
|
17
|
+
fieldName: 'note',
|
|
18
|
+
group: '',
|
|
19
|
+
label: 'Note',
|
|
20
|
+
type: InputType.Text
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
dataTestId: 'community',
|
|
24
|
+
fieldName: 'community',
|
|
25
|
+
group: '',
|
|
26
|
+
label: 'SNMP community',
|
|
27
|
+
type: InputType.Password
|
|
28
|
+
}
|
|
29
|
+
]}
|
|
30
|
+
submit={rs.fn()}
|
|
31
|
+
validationSchema={object()}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getInput = async (testId: string): Promise<HTMLInputElement> =>
|
|
37
|
+
(await screen.findAllByTestId(testId)).find(
|
|
38
|
+
(element) => element.tagName === 'INPUT'
|
|
39
|
+
) as HTMLInputElement;
|
|
40
|
+
|
|
41
|
+
describe('Password input', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
expect.hasAssertions();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('asks the browser and the password managers not to fill it', async () => {
|
|
47
|
+
initialize();
|
|
48
|
+
|
|
49
|
+
const password = await getInput('community');
|
|
50
|
+
|
|
51
|
+
expect(password).toHaveAttribute('type', 'password');
|
|
52
|
+
expect(password).toHaveAttribute('autocomplete', 'new-password');
|
|
53
|
+
expect(password).toHaveAttribute('data-1p-ignore', 'true');
|
|
54
|
+
expect(password).toHaveAttribute('data-bwignore', 'true');
|
|
55
|
+
expect(password).toHaveAttribute('data-lpignore', 'true');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('keeps those hints while the password is shown', async () => {
|
|
59
|
+
initialize();
|
|
60
|
+
|
|
61
|
+
await userEvent.click(
|
|
62
|
+
await screen.findByRole('button', { name: /password|show|visib/i })
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const password = await getInput('community');
|
|
66
|
+
|
|
67
|
+
expect(password).toHaveAttribute('type', 'text');
|
|
68
|
+
expect(password).toHaveAttribute('autocomplete', 'new-password');
|
|
69
|
+
expect(password).toHaveAttribute('data-1p-ignore', 'true');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('leaves the other text inputs as they were', async () => {
|
|
73
|
+
initialize();
|
|
74
|
+
|
|
75
|
+
const note = await getInput('note');
|
|
76
|
+
|
|
77
|
+
expect(note).toHaveAttribute('autocomplete', 'off');
|
|
78
|
+
expect(note).not.toHaveAttribute('data-1p-ignore');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -79,7 +79,11 @@ describe('Segmented buttons', () => {
|
|
|
79
79
|
it('displays every option and marks the current one', async () => {
|
|
80
80
|
initialize();
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const label = await screen.findByText('Notification Enabled');
|
|
83
|
+
|
|
84
|
+
expect(label).toBeVisible();
|
|
85
|
+
// Bold, as the design shows the label above the buttons.
|
|
86
|
+
expect(getComputedStyle(label).fontWeight).toBe('700');
|
|
83
87
|
expect(await button('Yes')).toHaveAttribute('aria-pressed', 'false');
|
|
84
88
|
expect(await button('No')).toHaveAttribute('aria-pressed', 'false');
|
|
85
89
|
expect(await button('Default')).toHaveAttribute('aria-pressed', 'true');
|
|
@@ -110,42 +114,51 @@ describe('Segmented buttons', () => {
|
|
|
110
114
|
expect(await button('No')).toHaveAttribute('aria-pressed', 'true');
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
it('sizes to its
|
|
114
|
-
initialize({
|
|
117
|
+
it('sizes each segment to its label, at least 104px, whatever the column', async () => {
|
|
118
|
+
initialize({
|
|
119
|
+
columnWidth: 800,
|
|
120
|
+
segmentedOptions: [
|
|
121
|
+
{ label: 'Yes', value: 'true' },
|
|
122
|
+
{ label: 'No', value: 'false' },
|
|
123
|
+
{ label: 'Inherit from template', value: 'use_default' }
|
|
124
|
+
]
|
|
125
|
+
});
|
|
115
126
|
|
|
116
127
|
const group = await screen.findByTestId('enabled');
|
|
117
|
-
const
|
|
118
|
-
await Promise.all(['Yes', 'No', '
|
|
128
|
+
const [yes, no, inherit] = (
|
|
129
|
+
await Promise.all(['Yes', 'No', 'Inherit from template'].map(button))
|
|
119
130
|
).map((element) => element.getBoundingClientRect().width);
|
|
120
131
|
|
|
121
|
-
//
|
|
122
|
-
// (
|
|
123
|
-
expect(
|
|
124
|
-
expect(Math.
|
|
125
|
-
expect(
|
|
132
|
+
// As in the design: Yes and No keep 104px, the longer label grows alone
|
|
133
|
+
// (the group overlaps the borders of its buttons, a pixel at most).
|
|
134
|
+
expect(yes).toBeGreaterThanOrEqual(104);
|
|
135
|
+
expect(Math.abs(yes - no)).toBeLessThanOrEqual(1);
|
|
136
|
+
expect(yes).toBeLessThan(110);
|
|
137
|
+
expect(inherit).toBeGreaterThan(yes + 20);
|
|
138
|
+
expect(group.getBoundingClientRect().width).toBeLessThan(500);
|
|
126
139
|
expect(group.getBoundingClientRect().left).toBe(
|
|
127
140
|
(await screen.findByText('Notification Enabled')).getBoundingClientRect()
|
|
128
141
|
.left
|
|
129
142
|
);
|
|
130
143
|
});
|
|
131
144
|
|
|
132
|
-
it('
|
|
133
|
-
initialize({ columnWidth:
|
|
145
|
+
it('keeps three segments of 104px when the labels are short', async () => {
|
|
146
|
+
initialize({ columnWidth: 800 });
|
|
134
147
|
|
|
135
|
-
const
|
|
136
|
-
|
|
148
|
+
const widths = (
|
|
149
|
+
await Promise.all(['Yes', 'No', 'Default'].map(button))
|
|
150
|
+
).map((element) => element.getBoundingClientRect().width);
|
|
137
151
|
|
|
138
|
-
expect(
|
|
139
|
-
expect(
|
|
140
|
-
expect((await button('Default')).scrollWidth).toBeLessThanOrEqual(
|
|
141
|
-
(await button('Default')).clientWidth
|
|
142
|
-
);
|
|
152
|
+
expect(Math.min(...widths)).toBeGreaterThanOrEqual(104);
|
|
153
|
+
expect(Math.max(...widths) - Math.min(...widths)).toBeLessThanOrEqual(1);
|
|
143
154
|
});
|
|
144
155
|
|
|
145
|
-
it('
|
|
146
|
-
const longLabel =
|
|
156
|
+
it('cuts a label wider than the room left with an ellipsis', async () => {
|
|
157
|
+
const longLabel =
|
|
158
|
+
'Vorlage erben und alle Werte der Vorlage ohne Ausnahme übernehmen';
|
|
147
159
|
|
|
148
160
|
initialize({
|
|
161
|
+
columnWidth: 420,
|
|
149
162
|
segmentedOptions: [
|
|
150
163
|
{ label: 'Yes', value: 'true' },
|
|
151
164
|
{ label: 'No', value: 'false' },
|
|
@@ -153,16 +166,26 @@ describe('Segmented buttons', () => {
|
|
|
153
166
|
]
|
|
154
167
|
});
|
|
155
168
|
|
|
156
|
-
const yes = await button('Yes');
|
|
157
169
|
const long = await button(longLabel);
|
|
170
|
+
const text = long.firstElementChild as HTMLElement;
|
|
158
171
|
|
|
159
|
-
expect(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
).toBeLessThanOrEqual(1);
|
|
164
|
-
expect(long.scrollWidth).toBeLessThanOrEqual(long.clientWidth);
|
|
172
|
+
expect(text.scrollWidth).toBeGreaterThan(text.clientWidth);
|
|
173
|
+
expect(getComputedStyle(text).textOverflow).toBe('ellipsis');
|
|
174
|
+
expect(long).toHaveAttribute('title', longLabel);
|
|
165
175
|
expect(long.getBoundingClientRect().height).toBe(28);
|
|
176
|
+
expect(
|
|
177
|
+
(await screen.findByTestId('enabled')).getBoundingClientRect().right
|
|
178
|
+
).toBeLessThanOrEqual(420 + 1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('stays inside a column narrower than its segments', async () => {
|
|
182
|
+
initialize({ columnWidth: 200 });
|
|
183
|
+
|
|
184
|
+
const group = await screen.findByTestId('enabled');
|
|
185
|
+
const wrapper = group.parentElement as HTMLElement;
|
|
186
|
+
|
|
187
|
+
expect(wrapper.getBoundingClientRect().width).toBeLessThanOrEqual(200);
|
|
188
|
+
expect(wrapper.scrollWidth).toBeGreaterThan(wrapper.clientWidth);
|
|
166
189
|
});
|
|
167
190
|
|
|
168
191
|
it('does not change when disabled', async () => {
|
|
@@ -32,15 +32,13 @@ const SegmentedButtons = ({
|
|
|
32
32
|
return useMemoComponent({
|
|
33
33
|
Component: (
|
|
34
34
|
<FormGroup>
|
|
35
|
-
<FormLabel>{t(label)}</FormLabel>
|
|
36
|
-
{/* In a column narrower than the
|
|
37
|
-
rather than overflowing into what is next to it. */}
|
|
35
|
+
<FormLabel className="font-bold">{t(label)}</FormLabel>
|
|
36
|
+
{/* In a column narrower than the three minimum widths, the group scrolls
|
|
37
|
+
inside it rather than overflowing into what is next to it. */}
|
|
38
38
|
<div className="max-w-full self-start overflow-x-auto">
|
|
39
39
|
<ButtonGroup
|
|
40
40
|
aria-label={t(label)}
|
|
41
|
-
|
|
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"
|
|
41
|
+
className="max-w-full"
|
|
44
42
|
color="primary"
|
|
45
43
|
data-testid={dataTestId}
|
|
46
44
|
disabled={disabled}
|
|
@@ -50,16 +48,21 @@ const SegmentedButtons = ({
|
|
|
50
48
|
({ label: optionLabel, value: optionValue }) => {
|
|
51
49
|
const isSelected = equals(value, optionValue);
|
|
52
50
|
|
|
51
|
+
// As in the design: every segment is 26 spacing units (104px)
|
|
52
|
+
// wide at least, and grows with its own label. A label wider
|
|
53
|
+
// than the room left is cut with an ellipsis, its full text
|
|
54
|
+
// in the tooltip.
|
|
53
55
|
return (
|
|
54
56
|
<Button
|
|
55
57
|
aria-pressed={isSelected}
|
|
56
|
-
className="h-7
|
|
58
|
+
className="h-7 min-w-26 px-3 py-0 font-medium text-sm leading-[21px] normal-case"
|
|
57
59
|
data-testid={`${dataTestId}-${optionValue}`}
|
|
58
60
|
key={optionValue}
|
|
59
61
|
onClick={select(optionValue)}
|
|
62
|
+
title={t(optionLabel)}
|
|
60
63
|
variant={isSelected ? 'contained' : 'outlined'}
|
|
61
64
|
>
|
|
62
|
-
{t(optionLabel)}
|
|
65
|
+
<span className="min-w-0 truncate">{t(optionLabel)}</span>
|
|
63
66
|
</Button>
|
|
64
67
|
);
|
|
65
68
|
}
|
package/src/Form/Inputs/Text.tsx
CHANGED
|
@@ -17,6 +17,17 @@ import { TextField, useMemoComponent } from '../..';
|
|
|
17
17
|
import { type InputPropsWithoutGroup, InputType } from './models';
|
|
18
18
|
import PasswordEndAdornment from './PasswordEndAdornment';
|
|
19
19
|
|
|
20
|
+
// Keeps password managers from taking the text field before a password field
|
|
21
|
+
// for a user name and filling it in, and from filling the password itself:
|
|
22
|
+
// `new-password` for the browsers, the opt-out attributes for the extensions
|
|
23
|
+
// (1Password, Bitwarden, LastPass).
|
|
24
|
+
const passwordManagerOptOut = {
|
|
25
|
+
autoComplete: 'new-password',
|
|
26
|
+
'data-1p-ignore': true,
|
|
27
|
+
'data-bwignore': true,
|
|
28
|
+
'data-lpignore': true
|
|
29
|
+
};
|
|
30
|
+
|
|
20
31
|
const Text = ({
|
|
21
32
|
dataTestId,
|
|
22
33
|
label,
|
|
@@ -140,7 +151,8 @@ const Text = ({
|
|
|
140
151
|
htmlInput: {
|
|
141
152
|
'aria-label': label,
|
|
142
153
|
'data-testid': dataTestId || label,
|
|
143
|
-
min: text?.min
|
|
154
|
+
min: text?.min,
|
|
155
|
+
...(equals(type, InputType.Password) && passwordManagerOptOut)
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
158
|
}}
|