fix(richtext-lexical): toolbar dropdown items are disabled when selecting via double-click (#13544)

Fixes https://github.com/payloadcms/payload/issues/13275 by ensuring
that toolbar styles are updated on mount.

This PR also improves the lexical test suite by adding data attributes
that can be targeted more easily

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211110462564657
This commit is contained in:
Alessio Gravili
2025-08-22 01:44:55 -07:00
committed by GitHub
parent ddb8ca4de2
commit 3bc1d0895f
9 changed files with 142 additions and 12 deletions

View File

@@ -23,6 +23,7 @@ const { serverURL } = await initPayloadE2ENoConfig({
})
describe('Lexical Fully Featured', () => {
let lexical: LexicalHelpers
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
@@ -37,14 +38,13 @@ describe('Lexical Fully Featured', () => {
uploadsDir: [path.resolve(dirname, './collections/Upload/uploads')],
})
const url = new AdminUrlUtil(serverURL, lexicalFullyFeaturedSlug)
const lexical = new LexicalHelpers(page)
lexical = new LexicalHelpers(page)
await page.goto(url.create)
await lexical.editor.first().focus()
})
test('prevent extra paragraph when inserting decorator blocks like blocks or upload node', async ({
page,
}) => {
const lexical = new LexicalHelpers(page)
await lexical.slashCommand('block')
await lexical.slashCommand('relationship')
await lexical.drawer.locator('.list-drawer__header').getByText('Create New').click()
@@ -68,7 +68,6 @@ describe('Lexical Fully Featured', () => {
test('ControlOrMeta+A inside input should select all the text inside the input', async ({
page,
}) => {
const lexical = new LexicalHelpers(page)
await lexical.editor.first().focus()
await page.keyboard.type('Hello')
await page.keyboard.press('Enter')
@@ -85,15 +84,50 @@ describe('Lexical Fully Featured', () => {
test('text state feature', async ({ page }) => {
await page.keyboard.type('Hello')
await page.keyboard.press('ControlOrMeta+A')
await page.locator('.toolbar-popup__dropdown-textState').first().click()
await page.getByRole('button', { name: 'Red' }).first().click()
await lexical.clickInlineToolbarButton({
dropdownKey: 'textState',
buttonKey: 'bg-red',
})
const colored = page.locator('span').filter({ hasText: 'Hello' })
await expect(colored).toHaveCSS('background-color', 'oklch(0.704 0.191 22.216)')
await expect(colored).toHaveAttribute('data-color', 'bg-red')
await page.locator('.toolbar-popup__dropdown-textState').first().click()
await page.getByRole('button', { name: 'Default style' }).click()
await expect(colored).toHaveAttribute('data-background-color', 'bg-red')
await lexical.clickInlineToolbarButton({
dropdownKey: 'textState',
buttonKey: 'clear-style',
})
await expect(colored).toBeVisible()
await expect(colored).not.toHaveCSS('background-color', 'oklch(0.704 0.191 22.216)')
await expect(colored).not.toHaveAttribute('data-color', 'bg-red')
await expect(colored).not.toHaveAttribute('data-background-color', 'bg-red')
})
test('ensure inline toolbar items are updated when selecting word by double-clicking', async ({
page,
}) => {
await page.keyboard.type('Hello')
await page.getByText('Hello').first().dblclick()
const { dropdownItems } = await lexical.clickInlineToolbarButton({
dropdownKey: 'textState',
})
const someButton = dropdownItems!.locator(`[data-item-key="bg-red"]`)
await expect(someButton).toHaveAttribute('aria-disabled', 'false')
})
test('ensure fixed toolbar items are updated when selecting word by double-clicking', async ({
page,
}) => {
await page.keyboard.type('Hello')
await page.getByText('Hello').first().dblclick()
const { dropdownItems } = await lexical.clickFixedToolbarButton({
dropdownKey: 'textState',
})
const someButton = dropdownItems!.locator(`[data-item-key="bg-red"]`)
await expect(someButton).toHaveAttribute('aria-disabled', 'false')
})
})

View File

@@ -1,4 +1,4 @@
import type { Page } from 'playwright'
import type { Locator, Page } from 'playwright'
import { expect } from '@playwright/test'
@@ -29,6 +29,64 @@ export class LexicalHelpers {
await this.page.keyboard.type(text)
}
async clickFixedToolbarButton({
buttonKey,
dropdownKey,
}: {
buttonKey?: string
dropdownKey?: string
}): Promise<{
dropdownItems?: Locator
}> {
if (dropdownKey) {
await this.fixedToolbar.locator(`[data-dropdown-key="${dropdownKey}"]`).click()
const dropdownItems = this.page.locator(`.toolbar-popup__dropdown-items`)
await expect(dropdownItems).toBeVisible()
if (buttonKey) {
await dropdownItems.locator(`[data-item-key="${buttonKey}"]`).click()
}
return {
dropdownItems,
}
}
if (buttonKey) {
await this.fixedToolbar.locator(`[data-item-key="${buttonKey}"]`).click()
}
return {}
}
async clickInlineToolbarButton({
buttonKey,
dropdownKey,
}: {
buttonKey?: string
dropdownKey?: string
}): Promise<{
dropdownItems?: Locator
}> {
if (dropdownKey) {
await this.inlineToolbar.locator(`[data-dropdown-key="${dropdownKey}"]`).click()
const dropdownItems = this.page.locator(`.toolbar-popup__dropdown-items`)
await expect(dropdownItems).toBeVisible()
if (buttonKey) {
await dropdownItems.locator(`[data-item-key="${buttonKey}"]`).click()
}
return {
dropdownItems,
}
}
if (buttonKey) {
await this.inlineToolbar.locator(`[data-item-key="${buttonKey}"]`).click()
}
return {}
}
async save(container: 'document' | 'drawer') {
if (container === 'drawer') {
await this.drawer.getByText('Save').click()
@@ -64,6 +122,14 @@ export class LexicalHelpers {
return this.page.locator('[data-lexical-editor="true"]')
}
get fixedToolbar() {
return this.page.locator('.fixed-toolbar')
}
get inlineToolbar() {
return this.page.locator('.inline-toolbar-popup')
}
get paragraph() {
return this.editor.locator('p')
}