Skip to content

Commit cd734b0

Browse files
authored
fix(ui): fix row width bug (#7940)
Closes #7867 Problem: currently, setting an ```ts admin: { width: '30%' } ``` does not work for fields inside a row or similar (group, array etc.) Solution: when we render the field, we set a CSS variable `--field-width` with the value of `admin.width`. This allows us to calculate the correct width for a field in CSS by doing `flex: 0 1 var(--field-width);` It also allows us to properly handle `gap` with `flex-wrap: wrap;` Notes: added playwright tests to ensure widths are correctly rendered ![image](https://github.com/user-attachments/assets/0c0f11fc-2387-4f01-9298-a2613fceee22)
2 parents 6e61431 + 82a6841 commit cd734b0

File tree

11 files changed

+193
-33
lines changed

11 files changed

+193
-33
lines changed

packages/payload/src/fields/config/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ type Admin = {
264264
position?: 'sidebar'
265265
readOnly?: boolean
266266
style?: CSSProperties
267-
width?: string
267+
width?: CSSProperties['width']
268268
}
269269

270270
export type AdminClient = {
@@ -296,8 +296,8 @@ export type AdminClient = {
296296
hidden?: boolean
297297
position?: 'sidebar'
298298
readOnly?: boolean
299-
style?: CSSProperties
300-
width?: string
299+
style?: { '--field-width'?: CSSProperties['width'] } & CSSProperties
300+
width?: CSSProperties['width']
301301
}
302302

303303
export type Labels = {
@@ -802,7 +802,7 @@ export type UIField = {
802802
*/
803803
disableListColumn?: boolean
804804
position?: string
805-
width?: string
805+
width?: CSSProperties['width']
806806
}
807807
/** Extension point to add your custom data. Server only. */
808808
custom?: Record<string, any>

packages/ui/src/fields/Collapsible/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
'use client'
2-
import type { CollapsibleFieldClientComponent, DocumentPreferences } from 'payload'
2+
import type { AdminClient, CollapsibleFieldClientComponent, DocumentPreferences } from 'payload'
33

44
import React, { Fragment, useCallback, useEffect, useState } from 'react'
55

@@ -116,6 +116,11 @@ const CollapsibleFieldComponent: CollapsibleFieldClientComponent = (props) => {
116116

117117
const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing
118118

119+
const style: AdminClient['style'] = {
120+
...field.admin?.style,
121+
'--field-width': field.admin.width,
122+
}
123+
119124
return (
120125
<Fragment>
121126
<WatchChildErrors fields={fields} path={path} setErrorCount={setErrorCount} />
@@ -129,6 +134,7 @@ const CollapsibleFieldComponent: CollapsibleFieldClientComponent = (props) => {
129134
.filter(Boolean)
130135
.join(' ')}
131136
id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`}
137+
style={style}
132138
>
133139
<CollapsibleElement
134140
className={`${baseClass}__collapsible`}

packages/ui/src/fields/Password/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
StaticDescription,
1010
TextFieldClient,
1111
} from 'payload'
12-
import type { ChangeEvent } from 'react'
12+
import type { CSSProperties, ChangeEvent } from 'react'
1313
import type React from 'react'
1414
import type { MarkOptional } from 'ts-essentials'
1515

@@ -47,5 +47,5 @@ export type PasswordInputProps = {
4747
readonly showError?: boolean
4848
readonly style?: React.CSSProperties
4949
readonly value?: string
50-
readonly width?: string
50+
readonly width?: CSSProperties['width']
5151
}

packages/ui/src/fields/Row/index.scss

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@
44
.row__fields {
55
display: flex;
66
flex-wrap: wrap;
7-
gap: calc(var(--base) * 0.5);
7+
row-gap: calc(var(--base) * 0.8);
88

99
> * {
10-
flex-grow: 1;
10+
flex: 0 1 var(--field-width);
11+
}
12+
13+
// If there is more than one child, add inline-margins to space them out.
14+
&:has(> *:nth-child(2)) {
15+
margin-inline: calc(var(--base) / -4); // add negative margin to counteract the gap.
16+
17+
> * {
18+
flex: 0 1 calc(var(--field-width) - var(--base) * 0.5);
19+
margin-inline: calc(var(--base) / 4);
20+
}
1121
}
1222
}
1323

packages/ui/src/fields/Select/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export type SelectInputProps = {
4747
readonly showError?: boolean
4848
readonly style?: React.CSSProperties
4949
readonly value?: string | string[]
50-
readonly width?: string
50+
readonly width?: React.CSSProperties['width']
5151
}
5252

5353
export const SelectInput: React.FC<SelectInputProps> = (props) => {

packages/ui/src/fields/Text/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ export type TextInputProps = {
4141
readonly style?: React.CSSProperties
4242
readonly value?: string
4343
readonly valueToRender?: Option[]
44-
readonly width?: string
44+
readonly width?: React.CSSProperties['width']
4545
} & SharedTextFieldProps

packages/ui/src/fields/Textarea/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ export type TextAreaInputProps = {
3838
readonly style?: React.CSSProperties
3939
readonly value?: string
4040
readonly valueToRender?: string
41-
readonly width?: string
41+
readonly width?: React.CSSProperties['width']
4242
}

packages/ui/src/fields/Upload/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export type UploadInputProps = {
7575
readonly showError?: boolean
7676
readonly style?: React.CSSProperties
7777
readonly value?: (number | string)[] | (number | string)
78-
readonly width?: string
78+
readonly width?: React.CSSProperties['width']
7979
}
8080

8181
export function UploadInput(props: UploadInputProps) {

packages/ui/src/providers/Config/createClientConfig/fields.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ export const createClientField = ({
115115

116116
const serverProps = { serverProps: { field: incomingField } }
117117

118+
if ('admin' in incomingField && 'width' in incomingField.admin) {
119+
clientField.admin.style = {
120+
...clientField.admin.style,
121+
'--field-width': clientField.admin.width,
122+
width: undefined, // avoid needlessly adding this to the element's style attribute
123+
}
124+
} else {
125+
if (!(clientField.admin instanceof Object)) {
126+
clientField.admin = {}
127+
}
128+
if (!(clientField.admin.style instanceof Object)) {
129+
clientField.admin.style = {}
130+
}
131+
clientField.admin.style.flex = '1 1 auto'
132+
}
133+
118134
switch (incomingField.type) {
119135
case 'array':
120136
case 'group':
@@ -521,7 +537,7 @@ export const createClientFields = ({
521537
})
522538

523539
if (newField) {
524-
newClientFields.push({ ...newField })
540+
newClientFields.push(newField)
525541
}
526542
}
527543

test/fields/collections/Row/index.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,36 @@ const RowFields: CollectionConfig = {
5151
type: 'row',
5252
fields: [
5353
{
54-
label: 'Collapsible within a row',
54+
name: 'field_with_width_30_percent',
55+
label: 'Field with 30% width',
56+
type: 'text',
57+
admin: {
58+
width: '30%',
59+
},
60+
},
61+
{
62+
name: 'field_with_width_60_percent',
63+
label: 'Field with 60% width',
64+
type: 'text',
65+
admin: {
66+
width: '60%',
67+
},
68+
},
69+
{
70+
name: 'field_with_width_20_percent',
71+
label: 'Field with 20% width',
72+
type: 'text',
73+
admin: {
74+
width: '20%',
75+
},
76+
},
77+
],
78+
},
79+
{
80+
type: 'row',
81+
fields: [
82+
{
83+
label: 'Collapsible 30% width within a row',
5584
type: 'collapsible',
5685
fields: [
5786
{
@@ -60,6 +89,9 @@ const RowFields: CollectionConfig = {
6089
type: 'text',
6190
},
6291
],
92+
admin: {
93+
width: '30%',
94+
},
6395
},
6496
{
6597
label: 'Collapsible within a row',
@@ -74,6 +106,37 @@ const RowFields: CollectionConfig = {
74106
},
75107
],
76108
},
109+
{
110+
type: 'row',
111+
fields: [
112+
{
113+
label: 'Explicit 20% width within a row (A)',
114+
type: 'text',
115+
name: 'field_20_percent_width_within_row_a',
116+
admin: {
117+
width: '20%',
118+
},
119+
},
120+
{
121+
label: 'No set width within a row (B)',
122+
type: 'text',
123+
name: 'no_set_width_within_row_b',
124+
},
125+
{
126+
label: 'No set width within a row (C)',
127+
type: 'text',
128+
name: 'no_set_width_within_row_c',
129+
},
130+
{
131+
label: 'Explicit 20% width within a row (D)',
132+
type: 'text',
133+
name: 'field_20_percent_width_within_row_d',
134+
admin: {
135+
width: '20%',
136+
},
137+
},
138+
],
139+
},
77140
],
78141
}
79142

0 commit comments

Comments
 (0)