• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SAP / ui5-webcomponents-react / 5256780421

pending completion
5256780421

Pull #4714

github

web-flow
Merge de3ab2640 into d4ddb9c0c
Pull Request #4714: docs: add NextJS app router example

2642 of 3617 branches covered (73.04%)

5123 of 5923 relevant lines covered (86.49%)

16227.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.96
/packages/main/src/components/Form/index.tsx
1
'use client';
2,439✔
2

3
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
4
import { clsx } from 'clsx';
5
import type { CSSProperties, ElementType, ReactElement, ReactNode } from 'react';
6
import React, { Children, cloneElement, forwardRef, useEffect, useMemo, useRef, useState } from 'react';
7
import { createUseStyles } from 'react-jss';
8
import { FormBackgroundDesign, TitleLevel } from '../../enums/index.js';
9
import type { CommonProps } from '../../interfaces/index.js';
10
import { Title } from '../../webComponents/index.js';
11
import { FormGroupTitle } from '../FormGroup/FormGroupTitle.js';
12
import { styles } from './Form.jss.js';
13
import { FormContext } from './FormContext.js';
14

15
export interface FormPropTypes extends CommonProps {
16
  /**
17
   * Components that are placed into Form.
18
   *
19
   * __Note:__ Although this prop accepts all HTML Elements, it is strongly recommended that you only use `FormGroup` or `FormItem` in order to preserve the intended design.
20
   */
21
  children: ReactNode | ReactNode[];
22
  /**
23
   * Specifies the background color of the Form content.
24
   */
25
  backgroundDesign?: FormBackgroundDesign;
26
  /**
27
   * Form title
28
   */
29
  titleText?: string;
30
  /**
31
   * Form columns for small size (`< 600px`).
32
   * Must be a number between 1 and 12.
33
   *
34
   * Default Value: 1
35
   */
36
  columnsS?: number;
37
  /**
38
   * Form columns for medium size (`600px` - `1023px`).
39
   * Must be a number between 1 and 12.
40
   *
41
   * Default Value: 1
42
   *
43
   * __Note__: The number of columns for medium size must not be smaller than the number of columns for small size.
44
   */
45
  columnsM?: number;
46
  /**
47
   * Form columns for large size (`1024px` - `1439px`).
48
   * Must be a number between 1 and 12.
49
   *
50
   * Default Value: 1
51
   *
52
   * __Note:__ The number of columns for large size must not be smaller than the number of columns for medium size.
53
   */
54
  columnsL?: number;
55
  /**
56
   * Form columns for extra large size (`>= 1440px`).
57
   * Must be a number between 1 and 12.
58
   *
59
   * Default Value: 2
60
   *
61
   * __Note:__ The number of columns for extra large size must not be smaller than the number of columns for large size.
62
   */
63
  columnsXL?: number;
64

65
  /**
66
   * Default span for labels in small size (`< 600px`).
67
   * Must be a number between 1 and 12.
68
   *
69
   * Default Value: 12
70
   */
71
  labelSpanS?: number;
72
  /**
73
   * Default span for labels in medium size (`600px` - `1023px`).
74
   * Must be a number between 1 and 12.
75
   *
76
   * Default Value: 2
77
   */
78
  labelSpanM?: number;
79
  /**
80
   * Default span for labels in large size (`1024px` - `1439px`).
81
   * Must be a number between 1 and 12.
82
   *
83
   * Default Value: 4
84
   */
85
  labelSpanL?: number;
86
  /**
87
   * Default span for labels in extra large size (`>= 1440px`).
88
   * Must be a number between 1 and 12.
89
   *
90
   * Default Value: 4
91
   */
92
  labelSpanXL?: number;
93
  /**
94
   * Sets the components outer HTML tag.
95
   *
96
   * __Note:__ For TypeScript the types of `ref` are bound to the default tag name, if you change it you are responsible to set the respective types yourself.
97
   */
98
  as?: keyof HTMLElementTagNameMap;
99
}
100

101
const useStyles = createUseStyles(styles, { name: 'Form' });
271✔
102
/**
103
 * The `Form` component arranges labels and fields into groups and rows. There are different ways to visualize forms for different screen sizes.
104
 * It is possible to change the alignment of all labels by setting the CSS `align-items` property, per default all labels are centered.
105
 *
106
 * __Note:__ The `Form` calculates its width based on the available space of its container. If the container also dynamically adjusts its width to its contents, you must ensure that you specify a fixed width, either for the container or for the `Form` itself. (e.g. when used inside a 'popover').
107
 */
108
const Form = forwardRef<HTMLFormElement, FormPropTypes>((props, ref) => {
271✔
109
  const {
110
    as = 'form',
36✔
111
    backgroundDesign = FormBackgroundDesign.Transparent,
44✔
112
    children,
113
    columnsS = 1,
44✔
114
    columnsM = 1,
44✔
115
    columnsL = 1,
44✔
116
    columnsXL = 2,
44✔
117
    className,
118
    labelSpanS = 12,
44✔
119
    labelSpanM = 2,
44✔
120
    labelSpanL = 4,
44✔
121
    labelSpanXL = 4,
44✔
122
    titleText,
123
    ...rest
124
  } = props;
44✔
125

126
  const columnsMap = new Map();
44✔
127
  columnsMap.set('Phone', columnsS);
44✔
128
  columnsMap.set('Tablet', columnsM);
44✔
129
  columnsMap.set('Desktop', columnsL);
44✔
130
  columnsMap.set('LargeDesktop', columnsXL);
44✔
131

132
  const labelSpanMap = new Map();
44✔
133
  labelSpanMap.set('Phone', labelSpanS);
44✔
134
  labelSpanMap.set('Tablet', labelSpanM);
44✔
135
  labelSpanMap.set('Desktop', labelSpanL);
44✔
136
  labelSpanMap.set('LargeDesktop', labelSpanXL);
44✔
137

138
  const [componentRef, containerRef] = useSyncRef<HTMLFormElement>(ref);
44✔
139
  // use the window range set as first best guess, if not available use Desktop
140
  const [currentRange, setCurrentRange] = useState(Device.getCurrentRange()?.name ?? 'Desktop');
44!
141
  const lastRange = useRef(currentRange);
44✔
142

143
  useEffect(() => {
44✔
144
    const observer = new ResizeObserver(([form]) => {
44✔
145
      const rangeInfo = Device.getCurrentRange(form.contentRect.width);
43✔
146
      if (rangeInfo && lastRange.current !== rangeInfo.name) {
43!
147
        lastRange.current = rangeInfo.name;
148
        setCurrentRange(rangeInfo.name);
149
      }
150
    });
151

152
    if (containerRef.current) {
44✔
153
      observer.observe(containerRef.current);
44✔
154
    }
155

156
    return () => {
44✔
157
      observer.disconnect();
35✔
158
    };
159
  }, [containerRef]);
160

161
  const classes = useStyles();
44✔
162

163
  const currentNumberOfColumns = columnsMap.get(currentRange);
44✔
164
  const currentLabelSpan = labelSpanMap.get(currentRange);
44✔
165

166
  const formGroups = useMemo(() => {
44✔
167
    if (currentNumberOfColumns === 1) {
44✔
168
      return children;
24✔
169
    }
170

171
    const computedFormGroups = [];
20✔
172
    const childrenArray = Children.toArray(children);
20✔
173
    const rows = childrenArray.reduce((acc, val, idx) => {
20✔
174
      const columnIndex = Math.floor(idx / currentNumberOfColumns);
30✔
175
      acc[columnIndex] ??= [];
30✔
176
      acc[columnIndex].push(val);
30✔
177
      return acc;
30✔
178
    }, []) as ReactElement[][];
179

180
    const maxRowsPerRow: number[] = [];
20✔
181
    rows.forEach((rowGroup: ReactElement[], rowIndex) => {
20✔
182
      maxRowsPerRow[rowIndex] = Math.max(
20✔
183
        ...rowGroup.map((row) => {
184
          if ((row.type as any).displayName === 'FormItem') {
30✔
185
            return 1;
10✔
186
          }
187
          return Children.count(row.props.children) + 1;
20✔
188
        })
189
      );
190
    });
191

192
    let totalRowCount = 2;
20✔
193

194
    rows.forEach((formGroup: ReactElement[], rowIndex) => {
20✔
195
      const rowsForThisRow = maxRowsPerRow.at(rowIndex);
20✔
196
      formGroup.forEach((cell, columnIndex) => {
20✔
197
        const titleStyles: CSSProperties = {
30✔
198
          gridColumnStart: columnIndex * 12 + 1,
199
          gridRowStart: totalRowCount
200
        };
201

202
        if (cell?.props?.titleText) {
30✔
203
          computedFormGroups.push(
20✔
204
            <FormGroupTitle
205
              titleText={cell.props.titleText}
206
              style={titleStyles}
207
              key={`title-col-${columnIndex}-row-${totalRowCount}`}
208
            />
209
          );
210
        }
211

212
        for (let i = 0; i < rowsForThisRow; i++) {
30✔
213
          let itemToRender;
214
          if ((cell.type as any).displayName === 'FormGroup') {
70✔
215
            itemToRender = Children.toArray(cell.props.children).at(i);
60✔
216
          } else if ((cell.type as any).displayName === 'FormItem' && i === 0) {
10✔
217
            // render a single FormItem only when index is 0
218
            itemToRender = cell;
10✔
219
          }
220

221
          if (itemToRender) {
70✔
222
            computedFormGroups.push(
50✔
223
              cloneElement(itemToRender as ReactElement, {
224
                key: `col-${columnIndex}-row-${totalRowCount + i}`,
225
                columnIndex,
226
                rowIndex: totalRowCount + i + 1
227
              })
228
            );
229
          }
230
        }
231
      });
232
      totalRowCount += rowsForThisRow;
20✔
233
      if (rowsForThisRow === 1) {
20✔
234
        totalRowCount += 1;
10✔
235
      }
236
    });
237

238
    return computedFormGroups;
20✔
239
  }, [children, currentNumberOfColumns]);
240

241
  const formClassNames = clsx(classes.form, classes[backgroundDesign.toLowerCase()]);
44✔
242

243
  const CustomTag = as as ElementType;
44✔
244
  return (
44✔
245
    <FormContext.Provider value={{ labelSpan: currentLabelSpan }}>
246
      <CustomTag
247
        className={clsx(classes.formContainer, className)}
248
        suppressHydrationWarning={true}
249
        ref={componentRef}
250
        {...rest}
251
      >
252
        <div
253
          className={formClassNames}
254
          style={
255
            {
256
              '--ui5wcr_form_label_span_s': labelSpanS,
257
              '--ui5wcr_form_label_span_m': labelSpanM,
258
              '--ui5wcr_form_label_span_l': labelSpanL,
259
              '--ui5wcr_form_label_span_xl': labelSpanXL,
260
              '--ui5wcr_form_columns_s': columnsS,
261
              '--ui5wcr_form_columns_m': columnsM,
262
              '--ui5wcr_form_columns_l': columnsL,
263
              '--ui5wcr_form_columns_xl': columnsXL
264
            } as CSSProperties
265
          }
266
        >
267
          {titleText && (
78✔
268
            <Title level={TitleLevel.H3} className={classes.formTitle}>
269
              {titleText}
270
            </Title>
271
          )}
272
          {formGroups}
273
        </div>
274
      </CustomTag>
275
    </FormContext.Provider>
276
  );
277
});
271✔
278

279
Form.displayName = 'Form';
271✔
280

281
export { Form };
542✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc