@centreon/ui 26.12.7 → 26.12.9

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.7",
3
+ "version": "26.12.9",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -1,7 +1,7 @@
1
1
  import { object } from 'yup';
2
2
 
3
3
  import { Form, GroupDirection } from './Form';
4
- import { InputType } from './Inputs/models';
4
+ import { type InputProps, InputType } from './Inputs/models';
5
5
  import {
6
6
  type BasicForm,
7
7
  basicFormGroups,
@@ -149,3 +149,65 @@ export const segmentedButtons = (): JSX.Element => (
149
149
  validationSchema={object()}
150
150
  />
151
151
  );
152
+
153
+ const getSeparatedFields = (count: number): Array<InputProps> =>
154
+ Array.from({ length: count }, (_, index) => ({
155
+ fieldName: `field${index + 1}`,
156
+ group: '',
157
+ label: `Field ${index + 1}`,
158
+ type: InputType.Text
159
+ }));
160
+
161
+ const getSeparatedGrid = ({
162
+ className,
163
+ count,
164
+ name
165
+ }: {
166
+ className: string;
167
+ count: number;
168
+ name: string;
169
+ }): InputProps => ({
170
+ fieldName: name,
171
+ grid: {
172
+ className,
173
+ columnSeparator: true,
174
+ columns: getSeparatedFields(count).map(({ group, ...field }) => ({
175
+ ...field,
176
+ fieldName: `${name}-${field.fieldName}`
177
+ }))
178
+ },
179
+ group: '',
180
+ label: name,
181
+ type: InputType.Grid
182
+ });
183
+
184
+ // The grids of this story show 1, 2 or 3 columns as the panel is resized (drag
185
+ // its bottom right corner): the lines only come between columns, on every row.
186
+ export const columnSeparators = (): JSX.Element => (
187
+ <div className="@container resize-x overflow-auto border border-dashed p-4">
188
+ <Form
189
+ initialValues={{}}
190
+ inputs={[
191
+ getSeparatedGrid({
192
+ className: 'grid-cols-1 gap-x-8 @[500px]:grid-cols-2',
193
+ count: 2,
194
+ name: 'two-columns'
195
+ }),
196
+ getSeparatedGrid({
197
+ className:
198
+ 'grid-cols-1 gap-x-8 @[500px]:grid-cols-2 @[900px]:grid-cols-3',
199
+ count: 3,
200
+ name: 'three-columns'
201
+ }),
202
+ getSeparatedGrid({
203
+ className:
204
+ 'grid-cols-1 gap-x-8 @[500px]:grid-cols-2 @[900px]:grid-cols-3',
205
+ count: 5,
206
+ name: 'wrapping-rows'
207
+ })
208
+ ]}
209
+ submit={submit}
210
+ validationSchema={object()}
211
+ />
212
+ </div>
213
+ );
@@ -0,0 +1,186 @@
1
+ import { rs } from '@rstest/core';
2
+ import { render } from '@test-setup/render';
3
+ import { screen } from '@testing-library/react';
4
+ import { object } from 'yup';
5
+
6
+ import { Form } from '../Form';
7
+ import { type InputProps, InputType } from './models';
8
+
9
+ const getField = (
10
+ name: string,
11
+ extra: Partial<InputProps> = {}
12
+ ): Omit<InputProps, 'group'> => ({
13
+ fieldName: name,
14
+ label: name,
15
+ type: InputType.Text,
16
+ ...extra
17
+ });
18
+
19
+ interface InitializeProps {
20
+ columnSeparator?: boolean;
21
+ // The tracks of the grid: what makes the number of columns follow the width.
22
+ columns: string;
23
+ fields: Array<Omit<InputProps, 'group'>>;
24
+ width: number;
25
+ }
26
+
27
+ const initialize = ({
28
+ columns,
29
+ columnSeparator,
30
+ fields,
31
+ width
32
+ }: InitializeProps): void => {
33
+ render(
34
+ <div style={{ padding: 40, width }}>
35
+ <Form
36
+ initialValues={{}}
37
+ inputs={[
38
+ {
39
+ fieldName: 'layout',
40
+ grid: {
41
+ columnSeparator,
42
+ columns: fields,
43
+ gridTemplateColumns: columns
44
+ },
45
+ group: '',
46
+ label: 'layout',
47
+ type: InputType.Grid
48
+ }
49
+ ]}
50
+ submit={rs.fn()}
51
+ validationSchema={object()}
52
+ />
53
+ </div>
54
+ );
55
+ };
56
+
57
+ // The lines the grid draws, one for each of its items.
58
+ const getLines = (): Array<HTMLElement> =>
59
+ Array.from(document.querySelectorAll('[data-column-separator]'));
60
+
61
+ // A line is seen when what is painted at its place is the line itself: outside
62
+ // the grid, where it is clipped, there is something else.
63
+ const getVisibleLines = (): Array<boolean> =>
64
+ getLines().map((line) => {
65
+ const { left, top } = line.getBoundingClientRect();
66
+
67
+ return document.elementFromPoint(left + 0.5, top + 10) === line;
68
+ });
69
+
70
+ const threeFields = ['one', 'two', 'three'].map((name) => getField(name));
71
+
72
+ describe('Grid column separator', () => {
73
+ beforeEach(() => {
74
+ expect.hasAssertions();
75
+ });
76
+
77
+ it('draws nothing unless asked to', async () => {
78
+ initialize({
79
+ columns: 'repeat(2, 1fr)',
80
+ fields: threeFields,
81
+ width: 800
82
+ });
83
+
84
+ await screen.findByLabelText('one');
85
+
86
+ expect(document.querySelectorAll('[data-column-separator]')).toHaveLength(
87
+ 0
88
+ );
89
+ });
90
+
91
+ it('draws a dashed line between two columns, not left of the first', async () => {
92
+ initialize({
93
+ columnSeparator: true,
94
+ columns: 'repeat(2, 1fr)',
95
+ fields: [getField('one'), getField('two')],
96
+ width: 800
97
+ });
98
+
99
+ await screen.findByLabelText('one');
100
+
101
+ expect(getVisibleLines()).toEqual([false, true]);
102
+
103
+ const line = getComputedStyle(getLines()[1]);
104
+
105
+ expect(line.borderLeftStyle).toBe('dashed');
106
+ expect(line.borderLeftWidth).toBe('1px');
107
+ });
108
+
109
+ it('draws nothing when the layout has a single column', async () => {
110
+ initialize({
111
+ columnSeparator: true,
112
+ columns: 'repeat(auto-fill, minmax(500px, 1fr))',
113
+ fields: [getField('one'), getField('two')],
114
+ width: 400
115
+ });
116
+
117
+ await screen.findByLabelText('one');
118
+
119
+ expect(getVisibleLines()).toEqual([false, false]);
120
+ });
121
+
122
+ it('follows the number of columns the layout shows', async () => {
123
+ initialize({
124
+ columnSeparator: true,
125
+ columns: 'repeat(auto-fill, minmax(300px, 1fr))',
126
+ fields: threeFields,
127
+ width: 1100
128
+ });
129
+
130
+ await screen.findByLabelText('one');
131
+
132
+ expect(getVisibleLines()).toEqual([false, true, true]);
133
+ });
134
+
135
+ it('draws the lines of every row when the items wrap', async () => {
136
+ initialize({
137
+ columnSeparator: true,
138
+ columns: 'repeat(3, 1fr)',
139
+ fields: ['a', 'b', 'c', 'd', 'e'].map((name) => getField(name)),
140
+ width: 1100
141
+ });
142
+
143
+ await screen.findByLabelText('a');
144
+
145
+ expect(getVisibleLines()).toEqual([false, true, true, false, true]);
146
+ });
147
+
148
+ it('spans the height of the row, not of the item', async () => {
149
+ initialize({
150
+ columnSeparator: true,
151
+ columns: 'repeat(2, 1fr)',
152
+ fields: [
153
+ getField('short'),
154
+ getField('tall', { text: { multilineRows: 6 } })
155
+ ],
156
+ width: 800
157
+ });
158
+
159
+ await screen.findByLabelText('short');
160
+
161
+ const [short, tall] = getLines().map(
162
+ (item) => item.getBoundingClientRect().height
163
+ );
164
+
165
+ expect(tall).toBeGreaterThan(100);
166
+ expect(short).toBeCloseTo(tall, 0);
167
+ });
168
+
169
+ it('leaves no line for a hidden item and follows the items left', async () => {
170
+ initialize({
171
+ columnSeparator: true,
172
+ columns: 'repeat(2, 1fr)',
173
+ fields: [
174
+ getField('one'),
175
+ getField('hidden', { hideInput: () => true }),
176
+ getField('three')
177
+ ],
178
+ width: 800
179
+ });
180
+
181
+ await screen.findByLabelText('one');
182
+
183
+ expect(getLines()).toHaveLength(2);
184
+ expect(getVisibleLines()).toEqual([false, true]);
185
+ });
186
+ });
@@ -2,10 +2,36 @@ import { Box, Typography } from '@mui/material';
2
2
 
3
3
  import { type FormikValues, useFormikContext } from 'formik';
4
4
  import { isNotEmpty, isNotNil } from 'ramda';
5
+ import type { CSSProperties } from 'react';
5
6
 
6
7
  import { getInput } from '.';
7
8
  import type { InputPropsWithoutGroup } from './models';
8
9
 
10
+ // The line is drawn left of every item, in the middle of the gap, and the grid
11
+ // clips what falls out of it: the items of the first column lose theirs, so it
12
+ // never shows in a single column nor left of the first one, whatever the number
13
+ // of columns, without knowing it. The clip keeps 4px around the grid so that
14
+ // nothing the items draw outside their box is cut.
15
+ const columnGap = 'calc(var(--spacing) * 8)';
16
+
17
+ const columnSeparatorGridStyle = {
18
+ '--form-grid-column-gap': columnGap,
19
+ columnGap: 'var(--form-grid-column-gap)',
20
+ overflowClipMargin: '4px',
21
+ overflowX: 'clip'
22
+ } as CSSProperties;
23
+
24
+ // The dashed line of an item: half a gap to its left, over the height of its
25
+ // content.
26
+ const ColumnSeparator = (): JSX.Element => (
27
+ <span
28
+ aria-hidden
29
+ className="absolute inset-y-0 border-l border-dashed border-divider"
30
+ data-column-separator
31
+ style={{ left: 'calc(var(--form-grid-column-gap) / -2)' }}
32
+ />
33
+ );
34
+
9
35
  const Grid = ({
10
36
  grid,
11
37
  hideInput
@@ -17,6 +43,7 @@ const Grid = ({
17
43
  }
18
44
 
19
45
  const className = grid?.className || '';
46
+ const hasColumnSeparator = grid?.columnSeparator ?? false;
20
47
 
21
48
  return (
22
49
  <div
@@ -26,7 +53,8 @@ const Grid = ({
26
53
  gridTemplateColumns: className
27
54
  ? grid?.gridTemplateColumns || undefined
28
55
  : grid?.gridTemplateColumns ||
29
- `repeat(${grid?.columns.length || 1}, 1fr)`
56
+ `repeat(${grid?.columns.length || 1}, 1fr)`,
57
+ ...(hasColumnSeparator && columnSeparatorGridStyle)
30
58
  }}
31
59
  >
32
60
  {grid?.columns.map((field) => {
@@ -41,17 +69,37 @@ const Grid = ({
41
69
  return null;
42
70
  }
43
71
 
72
+ const title = field.additionalLabel && (
73
+ <Typography
74
+ className={`mb-1 text-base font-bold text-text-primary ${field?.additionalLabelClassName || ''}`}
75
+ variant="h6"
76
+ >
77
+ {field.additionalLabel}
78
+ </Typography>
79
+ );
80
+
81
+ if (!hasColumnSeparator) {
82
+ return (
83
+ <Box key={key as string} sx={{ width: '100%' }}>
84
+ {title}
85
+ <Input {...field} />
86
+ </Box>
87
+ );
88
+ }
89
+
90
+ // The item stretches to the height of its row and its content fills
91
+ // what its title leaves, so the line spans the whole row.
44
92
  return (
45
- <Box key={key as string} sx={{ width: '100%' }}>
46
- {field.additionalLabel && (
47
- <Typography
48
- className={`mb-1 text-base font-bold text-text-primary ${field?.additionalLabelClassName || ''}`}
49
- variant="h6"
50
- >
51
- {field.additionalLabel}
52
- </Typography>
53
- )}
54
- <Input {...field} />
93
+ <Box
94
+ className="flex flex-col self-stretch"
95
+ key={key as string}
96
+ sx={{ width: '100%' }}
97
+ >
98
+ {title}
99
+ <div className="relative flex-1">
100
+ <ColumnSeparator />
101
+ <Input {...field} />
102
+ </div>
55
103
  </Box>
56
104
  );
57
105
  })}
@@ -32,7 +32,9 @@ const SegmentedButtons = ({
32
32
  return useMemoComponent({
33
33
  Component: (
34
34
  <FormGroup>
35
- <FormLabel className="font-bold">{t(label)}</FormLabel>
35
+ {!segmentedButtons?.hideLabel && (
36
+ <FormLabel className="font-bold">{t(label)}</FormLabel>
37
+ )}
36
38
  {/* In a column narrower than the three minimum widths, the group scrolls
37
39
  inside it rather than overflowing into what is next to it. */}
38
40
  <div className="max-w-full self-start overflow-x-auto">
@@ -146,6 +146,10 @@ export interface InputProps {
146
146
  grid?: {
147
147
  alignItems?: string;
148
148
  className?: string;
149
+ // A dashed vertical line in the gap between two columns, never left of
150
+ // the first one, whatever the number of columns the layout shows. To set
151
+ // on the grids that place groups of fields side by side.
152
+ columnSeparator?: boolean;
149
153
  columns: Array<Omit<InputProps, 'group'>>;
150
154
  gridTemplateColumns?: string;
151
155
  };
@@ -171,6 +175,8 @@ export interface InputProps {
171
175
  };
172
176
  required?: boolean;
173
177
  segmentedButtons?: {
178
+ // Leaves the label out of the display; it stays the group's accessible name.
179
+ hideLabel?: boolean;
174
180
  options: Array<{
175
181
  label: string;
176
182
  value: string;