Skip to content

feat(ui): fixed toolbar group customization #12108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

ruslan-amboss
Copy link
Contributor

What

This PR introduces a comprehensive customization system for toolbar groups in the Lexical Rich Text Editor. It allows developers to override not just the order, but virtually any aspect of toolbar components (such as format, align, indent) through the FixedToolbarFeature configuration. Customizable properties include order, icons, group type, and more.

Why

Previously, toolbar group configurations were hardcoded in their respective components with no way to modify them without changing the source code. This made it difficult for developers to:

  1. Reorder toolbar components to match specific UX requirements
  2. Replace icons with custom ones to maintain design consistency
  3. Transform dropdown groups into button groups or vice versa
  4. Apply other customizations needed for specific projects

This enhancement provides full flexibility for tailoring the rich text editor interface while maintaining a clean and maintainable codebase.

How

The implementation consists of three key parts:

  1. Enhanced the FixedToolbarFeature API:

    • Added a new customGroups property to FixedToolbarFeatureProps that accepts a record mapping group keys to partial ToolbarGroup objects
    • These partial objects can override any property of the default toolbar group configuration
  2. Leveraged existing deep merge utility:

    • Used Payload's existing deepMerge utility to properly combine default configurations with custom overrides
    • This ensures that only specified properties are overridden while preserving all other default behaviors
  3. Applied customizations in the sanitization process:

    • Updated the sanitizeClientFeatures function to identify and apply custom group configurations
    • Applied deep merging before the sorting process to ensure proper ordering with customized configurations
    • Maintained backward compatibility for users who don't need customization

Usage Example

import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { CustomIcon } from './icons/CustomIcon'

{
  name: 'content',
  type: 'richText',
  admin: {
    features: [
      // Other features...
      FixedToolbarFeature({
        customGroups: {
            'text': {
              order: 10,
              ChildComponent: CustomIcon,
            },
            'format': {
              order: 15,
            },
            'add': {
              type: 'buttons',
              order: 20,
            },
        }
      })
    ]
  }
}

Demo

fixed-toolbar-customization.mov

Comment on lines 14 to 19
/**
* Custom configurations for toolbar groups
* Key is the group key (e.g. 'format', 'indent', 'align')
* Value is a partial ToolbarGroup object that will be merged with the default configuration
*/
customGroups?: Record<string, Partial<ToolbarGroup>>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolbarGroup is designed to be used on the client. To pass it as a property to FixedToolbarFeature, we could accept only the serializable subset of properties.
This would mean excluding isEnabled and items from ToolbarDropdownGroup. Theoretically, some subproperties of items could also be retained with a bit more complexity in the types.

Your example in the PR description/video works because you only used properties from that subset of serializable properties. Also, while it's probably not a problem since they're usually static icons, I would add to the tsdoc that ChildComponent must be a server component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying! That makes sense regarding the need for serialization.

@ruslan-amboss ruslan-amboss requested a review from GermanJablo May 14, 2025 12:08
* @note Props passed via customGroups must be serializable. Avoid using functions or dynamic components.
* ChildComponent, if provided, must be a serializable server component.
*/
customGroups?: Record<string, Partial<Omit<ToolbarGroup, 'isEnabled' | 'items'>>>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type makes your example not work

text: {
  order: 10,
  ChildComponent: SwapIcon, // Object literal may only specify known properties, and 'ChildComponent' does not exist in type 'Partial<Omit<ToolbarGroup, "isEnabled" | "items">>'.
},

Additionally, if the item contains type: "button", it should not allow ChildComponent or maxActiveItems

// this should give a ts error
hello: {
  key: 'hello',
  order: 1,
  type: 'buttons',
  maxActiveItems: 1,
},

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got the idea! Pushed the updated types, which pass your examples

type CustomDropdownGroup = Partial<Omit<ToolbarDropdownGroup, 'isEnabled' | 'items'>>
type CustomButtonsGroup = Partial<
  Omit<ToolbarButtonsGroup, 'ChildComponent' | 'isEnabled' | 'items' | 'maxActiveItems'>
>
type CustomGroup = CustomButtonsGroup | CustomDropdownGroup
export type CustomGroups = Record<string, CustomGroup>

@ruslan-amboss ruslan-amboss requested a review from GermanJablo May 14, 2025 15:17
Copy link
Contributor

@GermanJablo GermanJablo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@GermanJablo GermanJablo enabled auto-merge (squash) May 14, 2025 17:15
@GermanJablo GermanJablo merged commit b7b2b39 into payloadcms:main May 14, 2025
76 checks passed
kendelljoseph pushed a commit that referenced this pull request May 15, 2025
### What

This PR introduces a comprehensive customization system for toolbar
groups in the Lexical Rich Text Editor. It allows developers to override
not just the order, but virtually any aspect of toolbar components (such
as format, align, indent) through the `FixedToolbarFeature`
configuration. Customizable properties include order, icons, group type,
and more.

### Why

Previously, toolbar group configurations were hardcoded in their
respective components with no way to modify them without changing the
source code. This made it difficult for developers to:

1. Reorder toolbar components to match specific UX requirements
2. Replace icons with custom ones to maintain design consistency 
3. Transform dropdown groups into button groups or vice versa
4. Apply other customizations needed for specific projects

This enhancement provides full flexibility for tailoring the rich text
editor interface while maintaining a clean and maintainable codebase.

### How

The implementation consists of three key parts:

1. **Enhanced the FixedToolbarFeature API**:
- Added a new `customGroups` property to `FixedToolbarFeatureProps` that
accepts a record mapping group keys to partial `ToolbarGroup` objects
- These partial objects can override any property of the default toolbar
group configuration

2. **Leveraged existing deep merge utility**:
- Used Payload's existing `deepMerge` utility to properly combine
default configurations with custom overrides
- This ensures that only specified properties are overridden while
preserving all other default behaviors
3. **Applied customizations in the sanitization process**:
- Updated the `sanitizeClientFeatures` function to identify and apply
custom group configurations
- Applied deep merging before the sorting process to ensure proper
ordering with customized configurations
- Maintained backward compatibility for users who don't need
customization

### Usage Example

```typescript
import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { CustomIcon } from './icons/CustomIcon'

{
  name: 'content',
  type: 'richText',
  admin: {
    features: [
      // Other features...
      FixedToolbarFeature({
        customGroups: {
            'text': {
              order: 10,
              ChildComponent: CustomIcon,
            },
            'format': {
              order: 15,
            },
            'add': {
              type: 'buttons',
              order: 20,
            },
        }
      })
    ]
  }
}
```

### Demo


https://github.com/user-attachments/assets/c3a59b60-b6c2-4721-bbc0-4954bdf52625

---------

Co-authored-by: Germán Jabloñski <[email protected]>
Copy link
Contributor

🚀 This is included in version v3.38.0

kendelljoseph pushed a commit that referenced this pull request May 19, 2025
### What

This PR introduces a comprehensive customization system for toolbar
groups in the Lexical Rich Text Editor. It allows developers to override
not just the order, but virtually any aspect of toolbar components (such
as format, align, indent) through the `FixedToolbarFeature`
configuration. Customizable properties include order, icons, group type,
and more.

### Why

Previously, toolbar group configurations were hardcoded in their
respective components with no way to modify them without changing the
source code. This made it difficult for developers to:

1. Reorder toolbar components to match specific UX requirements
2. Replace icons with custom ones to maintain design consistency 
3. Transform dropdown groups into button groups or vice versa
4. Apply other customizations needed for specific projects

This enhancement provides full flexibility for tailoring the rich text
editor interface while maintaining a clean and maintainable codebase.

### How

The implementation consists of three key parts:

1. **Enhanced the FixedToolbarFeature API**:
- Added a new `customGroups` property to `FixedToolbarFeatureProps` that
accepts a record mapping group keys to partial `ToolbarGroup` objects
- These partial objects can override any property of the default toolbar
group configuration

2. **Leveraged existing deep merge utility**:
- Used Payload's existing `deepMerge` utility to properly combine
default configurations with custom overrides
- This ensures that only specified properties are overridden while
preserving all other default behaviors
3. **Applied customizations in the sanitization process**:
- Updated the `sanitizeClientFeatures` function to identify and apply
custom group configurations
- Applied deep merging before the sorting process to ensure proper
ordering with customized configurations
- Maintained backward compatibility for users who don't need
customization

### Usage Example

```typescript
import { FixedToolbarFeature } from '@payloadcms/richtext-lexical'
import { CustomIcon } from './icons/CustomIcon'

{
  name: 'content',
  type: 'richText',
  admin: {
    features: [
      // Other features...
      FixedToolbarFeature({
        customGroups: {
            'text': {
              order: 10,
              ChildComponent: CustomIcon,
            },
            'format': {
              order: 15,
            },
            'add': {
              type: 'buttons',
              order: 20,
            },
        }
      })
    ]
  }
}
```

### Demo


https://github.com/user-attachments/assets/c3a59b60-b6c2-4721-bbc0-4954bdf52625

---------

Co-authored-by: Germán Jabloñski <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants