@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "26.12.6",
3
+ "version": "26.12.7",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -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
- expect(await screen.findByText('Notification Enabled')).toBeVisible();
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 labels, not to its column', async () => {
114
- initialize({ columnWidth: 800 });
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 widths = (
118
- await Promise.all(['Yes', 'No', 'Default'].map(button))
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
- // 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);
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('stays inside a column narrower than its segments', async () => {
133
- initialize({ columnWidth: 200 });
145
+ it('keeps three segments of 104px when the labels are short', async () => {
146
+ initialize({ columnWidth: 800 });
134
147
 
135
- const group = await screen.findByTestId('enabled');
136
- const wrapper = group.parentElement as HTMLElement;
148
+ const widths = (
149
+ await Promise.all(['Yes', 'No', 'Default'].map(button))
150
+ ).map((element) => element.getBoundingClientRect().width);
137
151
 
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
- );
152
+ expect(Math.min(...widths)).toBeGreaterThanOrEqual(104);
153
+ expect(Math.max(...widths) - Math.min(...widths)).toBeLessThanOrEqual(1);
143
154
  });
144
155
 
145
- it('widens every segment to the longest label, without wrapping', async () => {
146
- const longLabel = 'Inherit from the host template';
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
- Math.abs(
161
- yes.getBoundingClientRect().width - long.getBoundingClientRect().width
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 segments, the group scrolls inside it
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
- // 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"
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 whitespace-nowrap px-3 py-0 font-medium text-sm leading-[21px] normal-case"
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
  }
@@ -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
  }}