@centreon/ui 26.12.5 → 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.5",
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",
@@ -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
+ );
@@ -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
+ });
@@ -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
 
@@ -73,7 +79,11 @@ describe('Segmented buttons', () => {
73
79
  it('displays every option and marks the current one', async () => {
74
80
  initialize();
75
81
 
76
- 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');
77
87
  expect(await button('Yes')).toHaveAttribute('aria-pressed', 'false');
78
88
  expect(await button('No')).toHaveAttribute('aria-pressed', 'false');
79
89
  expect(await button('Default')).toHaveAttribute('aria-pressed', 'true');
@@ -104,6 +114,80 @@ describe('Segmented buttons', () => {
104
114
  expect(await button('No')).toHaveAttribute('aria-pressed', 'true');
105
115
  });
106
116
 
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
+ });
126
+
127
+ const group = await screen.findByTestId('enabled');
128
+ const [yes, no, inherit] = (
129
+ await Promise.all(['Yes', 'No', 'Inherit from template'].map(button))
130
+ ).map((element) => element.getBoundingClientRect().width);
131
+
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);
139
+ expect(group.getBoundingClientRect().left).toBe(
140
+ (await screen.findByText('Notification Enabled')).getBoundingClientRect()
141
+ .left
142
+ );
143
+ });
144
+
145
+ it('keeps three segments of 104px when the labels are short', async () => {
146
+ initialize({ columnWidth: 800 });
147
+
148
+ const widths = (
149
+ await Promise.all(['Yes', 'No', 'Default'].map(button))
150
+ ).map((element) => element.getBoundingClientRect().width);
151
+
152
+ expect(Math.min(...widths)).toBeGreaterThanOrEqual(104);
153
+ expect(Math.max(...widths) - Math.min(...widths)).toBeLessThanOrEqual(1);
154
+ });
155
+
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';
159
+
160
+ initialize({
161
+ columnWidth: 420,
162
+ segmentedOptions: [
163
+ { label: 'Yes', value: 'true' },
164
+ { label: 'No', value: 'false' },
165
+ { label: longLabel, value: 'use_default' }
166
+ ]
167
+ });
168
+
169
+ const long = await button(longLabel);
170
+ const text = long.firstElementChild as HTMLElement;
171
+
172
+ expect(text.scrollWidth).toBeGreaterThan(text.clientWidth);
173
+ expect(getComputedStyle(text).textOverflow).toBe('ellipsis');
174
+ expect(long).toHaveAttribute('title', longLabel);
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);
189
+ });
190
+
107
191
  it('does not change when disabled', async () => {
108
192
  initialize({ disabled: true });
109
193
 
@@ -32,36 +32,43 @@ const SegmentedButtons = ({
32
32
  return useMemoComponent({
33
33
  Component: (
34
34
  <FormGroup>
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);
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
+ <div className="max-w-full self-start overflow-x-auto">
39
+ <ButtonGroup
40
+ aria-label={t(label)}
41
+ className="max-w-full"
42
+ color="primary"
43
+ data-testid={dataTestId}
44
+ disabled={disabled}
45
+ size="small"
46
+ >
47
+ {segmentedButtons?.options.map(
48
+ ({ label: optionLabel, value: optionValue }) => {
49
+ const isSelected = equals(value, optionValue);
49
50
 
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>
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.
55
+ return (
56
+ <Button
57
+ aria-pressed={isSelected}
58
+ className="h-7 min-w-26 px-3 py-0 font-medium text-sm leading-[21px] normal-case"
59
+ data-testid={`${dataTestId}-${optionValue}`}
60
+ key={optionValue}
61
+ onClick={select(optionValue)}
62
+ title={t(optionLabel)}
63
+ variant={isSelected ? 'contained' : 'outlined'}
64
+ >
65
+ <span className="min-w-0 truncate">{t(optionLabel)}</span>
66
+ </Button>
67
+ );
68
+ }
69
+ )}
70
+ </ButtonGroup>
71
+ </div>
65
72
  </FormGroup>
66
73
  ),
67
74
  memoProps: [value, disabled, segmentedButtons, additionalMemoProps]
@@ -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
  }}