feat: added support for conditional tabs (#8720)

Adds support for conditional tabs.

You can now add a `condition` function like other fields to each
individual tab's admin config like so:

```ts
{
  name: 'contentTab',
  admin: {
    condition: (data) => Boolean(data?.enableTab)
  }
}
```

This will toggle the individual tab's visibility in the document listing

### Example


https://github.com/user-attachments/assets/45cf9cfd-eaed-4dfe-8a32-1992385fd05c

This is an updated PR from
https://github.com/payloadcms/payload/pull/8406 thanks to @willviles

---------

Co-authored-by: Will Viles <will@willviles.com>
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
This commit is contained in:
Paul
2025-03-13 13:32:53 +00:00
committed by GitHub
parent e8064a3a0c
commit 878dc54579
12 changed files with 323 additions and 63 deletions

View File

@@ -126,12 +126,69 @@ describe('Tabs', () => {
test('should render array data within named tabs', async () => {
await navigateToDoc(page, url)
await switchTab(page, '.tabs-field__tab-button:nth-child(5)')
await switchTab(page, '.tabs-field__tab-button:has-text("Tab with Name")')
await expect(page.locator('#field-tab__array__0__text')).toHaveValue(
"Hello, I'm the first row, in a named tab",
)
})
test('should render conditional tab when checkbox is toggled', async () => {
await navigateToDoc(page, url)
const conditionalTabSelector = '.tabs-field__tab-button:text-is("Conditional Tab")'
const button = page.locator(conditionalTabSelector)
await expect(
async () => await expect(page.locator(conditionalTabSelector)).toHaveClass(/--hidden/),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
const checkboxSelector = `input#field-conditionalTabVisible`
await page.locator(checkboxSelector).check()
await expect(page.locator(checkboxSelector)).toBeChecked()
await expect(
async () => await expect(page.locator(conditionalTabSelector)).not.toHaveClass(/--hidden/),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
await switchTab(page, conditionalTabSelector)
await expect(
page.locator('label[for="field-conditionalTab__conditionalTabField"]'),
).toHaveCount(1)
})
test('should hide nested conditional tab when checkbox is toggled', async () => {
await navigateToDoc(page, url)
// Show the conditional tab
const conditionalTabSelector = '.tabs-field__tab-button:text-is("Conditional Tab")'
const checkboxSelector = `input#field-conditionalTabVisible`
await page.locator(checkboxSelector).check()
await switchTab(page, conditionalTabSelector)
// Now assert on the nested conditional tab
const nestedConditionalTabSelector = '.tabs-field__tab-button:text-is("Nested Conditional Tab")'
await expect(
async () =>
await expect(page.locator(nestedConditionalTabSelector)).not.toHaveClass(/--hidden/),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
const nestedCheckboxSelector = `input#field-conditionalTab__nestedConditionalTabVisible`
await page.locator(nestedCheckboxSelector).uncheck()
await expect(
async () => await expect(page.locator(nestedConditionalTabSelector)).toHaveClass(/--hidden/),
).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
test('should save preferences for tab order', async () => {
await page.goto(url.list)
@@ -139,7 +196,7 @@ describe('Tabs', () => {
const href = await firstItem.getAttribute('href')
await firstItem.click()
const regex = new RegExp(href.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
const regex = new RegExp(href!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
await page.waitForURL(regex)

View File

@@ -21,9 +21,103 @@ const TabsFields: CollectionConfig = {
'This should not collapse despite there being many tabs pushing the main fields open.',
},
},
{
name: 'conditionalTabVisible',
type: 'checkbox',
label: 'Toggle Conditional Tab',
admin: {
position: 'sidebar',
description:
'When active, the conditional tab should be visible. When inactive, it should be hidden.',
},
},
{
type: 'tabs',
tabs: [
{
name: 'conditionalTab',
label: 'Conditional Tab',
description: 'This tab should only be visible when the conditional field is checked.',
fields: [
{
name: 'conditionalTabField',
type: 'text',
label: 'Conditional Tab Field',
defaultValue:
'This field should only be visible when the conditional tab is visible.',
},
{
name: 'nestedConditionalTabVisible',
type: 'checkbox',
label: 'Toggle Nested Conditional Tab',
defaultValue: true,
admin: {
description:
'When active, the nested conditional tab should be visible. When inactive, it should be hidden.',
},
},
{
type: 'group',
name: 'conditionalTabGroup',
fields: [
{
type: 'text',
name: 'conditionalTabGroupTitle',
},
{
type: 'tabs',
tabs: [
{
// duplicate name as above, should not conflict with tab IDs in form-state, if it does then tests will fail
name: 'conditionalTab',
label: 'Duplicate conditional tab',
fields: [],
admin: {
condition: ({ conditionalTab }) =>
!!conditionalTab?.nestedConditionalTabVisible,
},
},
],
},
],
},
{
type: 'tabs',
tabs: [
{
label: 'Nested Unconditional Tab',
description: 'Description for a nested unconditional tab',
fields: [
{
name: 'nestedUnconditionalTabInput',
type: 'text',
},
],
},
{
label: 'Nested Conditional Tab',
description: 'Here is a description for a nested conditional tab',
fields: [
{
name: 'nestedConditionalTabInput',
type: 'textarea',
defaultValue:
'This field should only be visible when the nested conditional tab is visible.',
},
],
admin: {
condition: ({ conditionalTab }) =>
!!conditionalTab?.nestedConditionalTabVisible,
},
},
],
},
],
admin: {
condition: ({ conditionalTabVisible }) => !!conditionalTabVisible,
},
},
{
label: 'Tab with Array',
description: 'This tab has an array.',

View File

@@ -1831,6 +1831,19 @@ export interface TabsField {
* This should not collapse despite there being many tabs pushing the main fields open.
*/
sidebarField?: string | null;
/**
* When active, the conditional tab should be visible. When inactive, it should be hidden.
*/
conditionalTabVisible?: boolean | null;
conditionalTab?: {
conditionalTabField?: string | null;
/**
* When active, the nested conditional tab should be visible. When inactive, it should be hidden.
*/
nestedConditionalTabVisible?: boolean | null;
nestedUnconditionalTabInput?: string | null;
nestedConditionalTabInput?: string | null;
};
array: {
text: string;
id?: string | null;
@@ -3452,6 +3465,15 @@ export interface TabsFields2Select<T extends boolean = true> {
*/
export interface TabsFieldsSelect<T extends boolean = true> {
sidebarField?: T;
conditionalTabVisible?: T;
conditionalTab?:
| T
| {
conditionalTabField?: T;
nestedConditionalTabVisible?: T;
nestedUnconditionalTabInput?: T;
nestedConditionalTabInput?: T;
};
array?:
| T
| {