diff --git a/packages/payload/src/fields/config/types.ts b/packages/payload/src/fields/config/types.ts index 484895dfab..a6c59b7415 100644 --- a/packages/payload/src/fields/config/types.ts +++ b/packages/payload/src/fields/config/types.ts @@ -267,7 +267,7 @@ type Admin = { position?: 'sidebar' readOnly?: boolean style?: CSSProperties - width?: string + width?: `${string}${'%' | 'px'}` } export type AdminClient = { diff --git a/packages/ui/src/fields/Collapsible/index.tsx b/packages/ui/src/fields/Collapsible/index.tsx index 2b2a9fa664..e911de99c8 100644 --- a/packages/ui/src/fields/Collapsible/index.tsx +++ b/packages/ui/src/fields/Collapsible/index.tsx @@ -114,6 +114,11 @@ const CollapsibleFieldComponent: React.FC = (props) => { const disabled = readOnlyFromProps || readOnlyFromContext || formProcessing || formInitializing + const style: { '--field-width': string } & React.CSSProperties = { + ...field.admin?.style, + '--field-width': field.admin.width, + } + return ( @@ -127,6 +132,7 @@ const CollapsibleFieldComponent: React.FC = (props) => { .filter(Boolean) .join(' ')} id={`field-${fieldPreferencesKey}${path ? `-${path.replace(/\./g, '__')}` : ''}`} + style={style} > * { - flex-grow: 1; + flex: 0 1 var(--field-width); + --field-width: 100%; + } + + // If there is more than one child, add a gap. + // We ned this because if you use `flex-wrap: wrap` and `gap` + // and only have one child, the gap gets applied regardless + // meaning the child doesn't take up the full width. + &:has(> *:nth-child(2)) { + gap: calc(var(--base) * 0.5); + margin-inline-end: calc(var(--base) * -0.5); // add negative margin to counteract the gap. + + > * { + flex: 0 1 calc(var(--field-width) - var(--base) * 0.5); + } } } diff --git a/packages/ui/src/forms/RenderFields/RenderField.tsx b/packages/ui/src/forms/RenderFields/RenderField.tsx index c3af0b30fc..ed48a3b920 100644 --- a/packages/ui/src/forms/RenderFields/RenderField.tsx +++ b/packages/ui/src/forms/RenderFields/RenderField.tsx @@ -85,15 +85,14 @@ export const RenderField: React.FC = ({ /> ) } else { - if (fieldComponentProps.field.type === 'row') { + if ('fields' in fieldComponentProps.field && Array.isArray(fieldComponentProps.field.fields)) { for (const field of fieldComponentProps.field.fields) { if (field.admin?.width) { field.admin.style = { ...field.admin.style, - maxWidth: field.admin.width, - } - - field.admin.width = undefined + '--field-width': field.admin.width, + width: undefined, // avoid needlessly adding this to the element's style attribute + } as unknown as { '--field-width': string } & React.CSSProperties } } } diff --git a/test/fields/collections/Row/index.ts b/test/fields/collections/Row/index.ts index 0416c9cc82..95c9469954 100644 --- a/test/fields/collections/Row/index.ts +++ b/test/fields/collections/Row/index.ts @@ -47,6 +47,35 @@ const RowFields: CollectionConfig = { }, ], }, + { + type: 'row', + fields: [ + { + name: 'field_with_width_30_percent', + label: 'Field with 30% width', + type: 'text', + admin: { + width: '30%', + }, + }, + { + name: 'field_with_width_60_percent', + label: 'Field with 60% width', + type: 'text', + admin: { + width: '60%', + }, + }, + { + name: 'field_with_width_20_percent', + label: 'Field with 20% width', + type: 'text', + admin: { + width: '20%', + }, + }, + ], + }, { type: 'row', fields: [ @@ -60,6 +89,9 @@ const RowFields: CollectionConfig = { type: 'text', }, ], + admin: { + width: '30%', + }, }, { label: 'Collapsible within a row', diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index 3f6b0164d6..ddcfd87ce4 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -376,32 +376,47 @@ describe('fields', () => { const fieldA = page.locator('input#field-field_with_width_a') const fieldB = page.locator('input#field-field_with_width_b') - const fieldAGrandprent = fieldA.locator('..').locator('..') - const fieldBGrandprent = fieldB.locator('..').locator('..') - await expect(fieldA).toBeVisible() await expect(fieldB).toBeVisible() - const hasCorrectCSS = async (el: Locator) => { - return await el.evaluate((el) => { - return el.style.width === '' && el.style.maxWidth === '50%' - }) - } - - expect(hasCorrectCSS(fieldAGrandprent)).toBeTruthy() - expect(hasCorrectCSS(fieldBGrandprent)).toBeTruthy() - const fieldABox = await fieldA.boundingBox() const fieldBBox = await fieldB.boundingBox() - // Check that the top value of the fields are the same - // Give it some wiggle room of like 2px to account for differences in rendering - const tolerance = 2 - expect(fieldABox.y).toBeLessThanOrEqual(fieldBBox.y + tolerance) + await expect(() => { + expect(fieldABox.y).toEqual(fieldBBox.y) + expect(fieldABox.width).toEqual(fieldBBox.width) + }).toPass() - // Check that the widths of the fields are the same - const difference = Math.abs(fieldABox.width - fieldBBox.width) - expect(difference).toBeLessThanOrEqual(tolerance) + const field_30_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_30_percent)', + ) + const field_60_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_60_percent)', + ) + const field_20_percent = page.locator( + '.field-type.text:has(input#field-field_with_width_20_percent)', + ) + const collapsible_30_percent = page.locator( + '.collapsible-field:has(#field-field_within_collapsible_a)', + ) + + await expect(field_30_percent).toBeVisible() + await expect(field_60_percent).toBeVisible() + await expect(field_20_percent).toBeVisible() + await expect(collapsible_30_percent).toBeVisible() + + const field_30_boundingBox = await field_30_percent.boundingBox() + const field_60_boundingBox = await field_60_percent.boundingBox() + const field_20_boundingBox = await field_20_percent.boundingBox() + const collapsible_30_boundingBox = await collapsible_30_percent.boundingBox() + + await expect(() => { + expect(field_30_boundingBox.y).toEqual(field_60_boundingBox.y) + expect(field_30_boundingBox.x).toEqual(field_20_boundingBox.x) + expect(field_30_boundingBox.y).not.toEqual(field_20_boundingBox.y) + expect(field_30_boundingBox.height).toEqual(field_60_boundingBox.height) + expect(collapsible_30_boundingBox.width).toEqual(field_30_boundingBox.width) + }).toPass() }) test('should render nested row fields in the correct position', async () => {