diff --git a/packages/ui/src/fields/Select/Input.tsx b/packages/ui/src/fields/Select/Input.tsx index c9ae3e4afa..d51d0148e9 100644 --- a/packages/ui/src/fields/Select/Input.tsx +++ b/packages/ui/src/fields/Select/Input.tsx @@ -83,6 +83,9 @@ export const SelectInput: React.FC = (props) => { label: matchingOption ? getTranslation(matchingOption.label, i18n) : value, value: matchingOption?.value ?? value, } + } else { + // If value is not present then render nothing, allowing select fields to reset to their initial 'Select an option' state + valueToRender = null } return ( diff --git a/test/admin/collections/CustomFields/fields/Select/CustomInput.tsx b/test/admin/collections/CustomFields/fields/Select/CustomInput.tsx new file mode 100644 index 0000000000..c60ec55cd2 --- /dev/null +++ b/test/admin/collections/CustomFields/fields/Select/CustomInput.tsx @@ -0,0 +1,61 @@ +'use client' + +import type { OptionObject, UIField } from 'payload' + +import { SelectInput, useField } from '@payloadcms/ui' +import { useEffect, useMemo } from 'react' + +interface Props { + field: UIField + path: string + required?: boolean +} + +const selectOptions = [ + { + label: 'Option 1', + value: 'option-1', + }, + { + label: 'Option 2', + value: 'option-2', + }, +] +export function CustomInput({ field, path, required = false }: Props) { + const { setValue, value } = useField({ path }) + + const options = useMemo(() => { + const internal: OptionObject[] = [] + + internal.push(...selectOptions) + + return internal + }, []) + + return ( +
+ { + const selectedValue = (Array.isArray(option) ? option[0]?.value : option?.value) || '' + setValue(selectedValue) + }} + options={options} + path={path} + required={required} + value={value} + /> + +
+ ) +} diff --git a/test/admin/collections/CustomFields/index.ts b/test/admin/collections/CustomFields/index.ts index 122a39941d..ce5017c65a 100644 --- a/test/admin/collections/CustomFields/index.ts +++ b/test/admin/collections/CustomFields/index.ts @@ -73,6 +73,15 @@ export const CustomFields: CollectionConfig = { }, }, }, + { + name: 'customSelectInput', + type: 'text', + admin: { + components: { + Field: '/collections/CustomFields/fields/Select/CustomInput.js#CustomInput', + }, + }, + }, { name: 'relationshipFieldWithBeforeAfterInputs', type: 'relationship', diff --git a/test/admin/e2e/document-view/e2e.spec.ts b/test/admin/e2e/document-view/e2e.spec.ts index 218575613a..b4b92ee806 100644 --- a/test/admin/e2e/document-view/e2e.spec.ts +++ b/test/admin/e2e/document-view/e2e.spec.ts @@ -21,10 +21,10 @@ import { customEditLabel, customNestedTabViewPath, customNestedTabViewTitle, + customTabAdminDescription, customTabLabel, customTabViewPath, customTabViewTitle, - customTabAdminDescription, } from '../../shared.js' import { customFieldsSlug, @@ -274,9 +274,7 @@ describe('Document View', () => { test('List drawer should not effect underlying breadcrumbs', async () => { await navigateToDoc(page, postsUrl) - expect(await page.locator('.step-nav.app-header__step-nav a').nth(1).innerText()).toBe( - 'Posts', - ) + await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts') await page.locator('#field-upload button.upload__listToggler').click() await expect(page.locator('[id^=list-drawer_1_]')).toBeVisible() @@ -286,9 +284,7 @@ describe('Document View', () => { page.locator('.step-nav.app-header__step-nav .step-nav__last'), ).not.toContainText('Uploads') - expect(await page.locator('.step-nav.app-header__step-nav a').nth(1).innerText()).toBe( - 'Posts', - ) + await expect(page.locator('.step-nav.app-header__step-nav a').nth(1)).toHaveText('Posts') }) }) @@ -400,9 +396,9 @@ describe('Document View', () => { await page.waitForURL(postsUrl.create) const secondTab = page.locator('.tabs-field__tab-button').nth(1) - secondTab.click() + await secondTab.click() - wait(500) + await wait(500) const tabsContent = page.locator('.tabs-field__content-wrap') await expect( @@ -463,6 +459,24 @@ describe('Document View', () => { ).toBeVisible() }) + test('custom select input can have its value cleared', async () => { + await page.goto(customFieldsURL.create) + await page.waitForURL(customFieldsURL.create) + await expect(page.locator('#field-customSelectInput')).toBeVisible() + + await page.locator('#field-customSelectInput .rs__control').click() + await page.locator('#field-customSelectInput .rs__option').first().click() + + await expect(page.locator('#field-customSelectInput .rs__single-value')).toHaveText( + 'Option 1', + ) + + await page.locator('.clear-value').click() + await expect(page.locator('#field-customSelectInput .rs__placeholder')).toHaveText( + 'Select a value', + ) + }) + describe('field descriptions', () => { test('should render static field description', async () => { await page.goto(customFieldsURL.create) @@ -535,7 +549,7 @@ describe('Document View', () => { describe('publish button', () => { test('should show publish active locale button with defaultLocalePublishOption', async () => { await navigateToDoc(page, postsUrl) - const publishButton = await page.locator('#action-save') + const publishButton = page.locator('#action-save') await expect(publishButton).toBeVisible() await expect(publishButton).toContainText('Publish in English') }) diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index 55965db808..9f8ea08910 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -334,6 +334,7 @@ export interface CustomField { descriptionAsFunction?: string | null; descriptionAsComponent?: string | null; customSelectField?: string | null; + customSelectInput?: string | null; relationshipFieldWithBeforeAfterInputs?: (string | null) | Post; arrayFieldWithBeforeAfterInputs?: | { @@ -717,6 +718,7 @@ export interface CustomFieldsSelect { descriptionAsFunction?: T; descriptionAsComponent?: T; customSelectField?: T; + customSelectInput?: T; relationshipFieldWithBeforeAfterInputs?: T; arrayFieldWithBeforeAfterInputs?: | T