From accd95ec8a7fb08ad4246fc3403f3fef7d1ac34d Mon Sep 17 00:00:00 2001 From: Patrik <35232443+PatrikKozak@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:15:30 -0400 Subject: [PATCH] fix(ui): array fields not respecting width styles in row layouts (#13986) ### What? This PR applies `mergeFieldStyles` to the `ArrayFieldComponent` component, ensuring that custom admin styles such as `width` are correctly respected when Array fields are placed inside row layouts. ### Why? Previously, Array fields did not inherit or apply their `admin.width` (or other merged field styles). For example, when placing two array fields side by side inside a row with `width: '50%'`, the widths were ignored, causing layout issues. ### How? - Imported and used `mergeFieldStyles` within `ArrayFieldComponent`. - Applied the merged styles to the root `
` via the `style` prop, consistent with how other field components (like `TextField`) handle styles. Fixes #13973 --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1211511898438801 --- packages/ui/src/fields/Array/index.tsx | 7 +++- test/fields/collections/Row/e2e.spec.ts | 45 +++++++++++++++++++++++-- test/fields/collections/Row/index.ts | 31 +++++++++++++++++ test/fields/payload-types.ts | 24 +++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/fields/Array/index.tsx b/packages/ui/src/fields/Array/index.tsx index 848971467..0358f459a 100644 --- a/packages/ui/src/fields/Array/index.tsx +++ b/packages/ui/src/fields/Array/index.tsx @@ -6,7 +6,7 @@ import type { } from 'payload' import { getTranslation } from '@payloadcms/translations' -import React, { Fragment, useCallback } from 'react' +import React, { Fragment, useCallback, useMemo } from 'react' import { toast } from 'sonner' import type { ClipboardPasteData } from '../../elements/ClipboardAction/types.js' @@ -36,6 +36,7 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { scrollToID } from '../../utilities/scrollToID.js' +import { mergeFieldStyles } from '../mergeFieldStyles.js' import { fieldBaseClass } from '../shared/index.js' import { ArrayRow } from './ArrayRow.js' import './index.scss' @@ -44,6 +45,7 @@ const baseClass = 'array-field' export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { const { + field, field: { name, type, @@ -299,6 +301,8 @@ export const ArrayFieldComponent: ArrayFieldClientComponent = (props) => { const showRequired = (readOnly || disabled) && rows.length === 0 const showMinRows = (rows.length && rows.length < minRows) || (required && rows.length === 0) + const styles = useMemo(() => mergeFieldStyles(field), [field]) + return (
{ .filter(Boolean) .join(' ')} id={`field-${path.replace(/\./g, '__')}`} + style={styles} > {showError && ( { getComputedStyle(el).getPropertyValue('--field-width').trim(), ) - expect(leftVar).toBe('50%') - expect(rightVar).toBe('50%') + await expect(() => { + expect(leftVar).toBe('50%') + expect(rightVar).toBe('50%') + }).toPass() + + // Also assert inline style contains the var (robust to other inline styles) + await expect(left).toHaveAttribute('style', /--field-width:\s*50%/) + await expect(right).toHaveAttribute('style', /--field-width:\s*50%/) + + // 2) Layout reflects the widths (same row, equal widths) + const leftBox = await left.boundingBox() + const rightBox = await right.boundingBox() + + await expect(() => { + // Same row + expect(Math.round(leftBox.y)).toEqual(Math.round(rightBox.y)) + // Equal width (tolerate sub-pixel differences) + expect(Math.round(leftBox.width)).toEqual(Math.round(rightBox.width)) + }).toPass() + }) + + test('should respect admin.width for array fields inside a row', async () => { + await page.goto(url.create) + + // Target the Array field wrappers + const left = page.locator('#field-arrayLeftColumn') + const right = page.locator('#field-arrayRightColumn') + + await expect(left).toBeVisible() + await expect(right).toBeVisible() + + // 1) CSS variable is applied (via mergeFieldStyles) + const leftVar = await left.evaluate((el) => + getComputedStyle(el).getPropertyValue('--field-width').trim(), + ) + const rightVar = await right.evaluate((el) => + getComputedStyle(el).getPropertyValue('--field-width').trim(), + ) + + await expect(() => { + expect(leftVar).toBe('50%') + expect(rightVar).toBe('50%') + }).toPass() // Also assert inline style contains the var (robust to other inline styles) await expect(left).toHaveAttribute('style', /--field-width:\s*50%/) diff --git a/test/fields/collections/Row/index.ts b/test/fields/collections/Row/index.ts index c25d47089..1142a68af 100644 --- a/test/fields/collections/Row/index.ts +++ b/test/fields/collections/Row/index.ts @@ -178,6 +178,37 @@ const RowFields: CollectionConfig = { }, ], }, + { + type: 'row', + fields: [ + { + name: 'arrayLeftColumn', + type: 'array', + admin: { + width: '50%', + }, + fields: [ + { + name: 'leftArrayChild', + type: 'text', + }, + ], + }, + { + name: 'arrayRightColumn', + type: 'array', + fields: [ + { + name: 'rightArrayChild', + type: 'text', + }, + ], + admin: { + width: '50%', + }, + }, + ], + }, ], } diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 6de3e8529..50b383869 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -1194,6 +1194,18 @@ export interface RowField { blockType: 'rightTextBlock'; }[] | null; + arrayLeftColumn?: + | { + leftArrayChild?: string | null; + id?: string | null; + }[] + | null; + arrayRightColumn?: + | { + rightArrayChild?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -2904,6 +2916,18 @@ export interface RowFieldsSelect { blockName?: T; }; }; + arrayLeftColumn?: + | T + | { + leftArrayChild?: T; + id?: T; + }; + arrayRightColumn?: + | T + | { + rightArrayChild?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; }