Skip to content

Commit fed3bba

Browse files
authored
fix(richtext-lexical, ui): ui errors with Slash Menu in Lexical and SearchBar component in RTL (#14231)
Fixes #14162 (partially) This fixes two issues: - The SearchBarIcon. It was using left and right padding instead of inline-start and inline-end. - SlashMenu. This one is more complex. I left a fairly specific test because Lexical doesn't expose this logic and our fork has already diverged quite a bit. There's a third issue reported in the issue about checklists, but I've confirmed it's a Lexical regression that occurred in v0.35.0, as the bug is reproducible at https://playground.lexical.dev/. I've already [reported it](facebook/lexical#7929). Before <img width="278" height="102" alt="image" src="https://github.com/user-attachments/assets/efcb4deb-517d-4a3a-97d5-c28b0e981824" /> After <img width="322" height="154" alt="image" src="https://github.com/user-attachments/assets/af60cd88-93c8-4d6d-8695-b4caaf0e7054" /> Before <img width="282" height="390" alt="image" src="https://github.com/user-attachments/assets/2a51b419-37d4-4287-98c9-91b3f70dd06f" /> After <img width="359" height="454" alt="image" src="https://github.com/user-attachments/assets/20595c0a-674f-4960-8e94-7f55d0a0b258" />
1 parent 6d3aaaf commit fed3bba

File tree

6 files changed

+119
-6
lines changed

6 files changed

+119
-6
lines changed

packages/richtext-lexical/src/lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/LexicalMenu.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,15 @@ export function useMenuAnchorRef(
499499

500500
const rootElementRect = rootElement.getBoundingClientRect()
501501

502-
if (left + menuWidth > rootElementRect.right) {
502+
const isRTL = document.dir === 'rtl' || document.documentElement.dir === 'rtl'
503+
const anchorRect = anchorElem.getBoundingClientRect()
504+
const leftBoundary = Math.max(0, rootElementRect.left)
505+
506+
if (!isRTL && left + menuWidth > rootElementRect.right) {
503507
containerDiv.style.left = `${rootElementRect.right - menuWidth + window.scrollX}px`
508+
} else if (isRTL && menuRect.left < leftBoundary) {
509+
const newLeft = leftBoundary + menuWidth - anchorRect.left
510+
containerDiv.style.left = `${newLeft + window.scrollX}px`
504511
}
505512

506513
const wouldGoOffBottomOfScreen = rawTop + menuHeight + VERTICAL_OFFSET > window.innerHeight

packages/ui/src/elements/SearchBar/index.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939
border-radius: inherit;
4040
input {
4141
height: 100%;
42-
padding: calc(var(--base) * 0.5) var(--base) calc(var(--base) * 0.5) var(--icon-width);
42+
padding-bottom: calc(var(--base) * 0.5);
43+
padding-top: calc(var(--base) * 0.5);
44+
padding-inline-start: var(--icon-width);
45+
padding-inline-end: var(--base);
4346
background-color: transparent;
4447
}
4548
}

test/lexical/baseConfig.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { en } from '@payloadcms/translations/languages/en'
2+
import { es } from '@payloadcms/translations/languages/es'
3+
import { he } from '@payloadcms/translations/languages/he'
14
import { fileURLToPath } from 'node:url'
25
import path from 'path'
36
import { type Config } from 'payload'
@@ -85,7 +88,14 @@ export const baseConfig: Partial<Config> = {
8588
localization: {
8689
defaultLocale: 'en',
8790
fallback: true,
88-
locales: ['en', 'es'],
91+
locales: ['en', 'es', 'he'],
92+
},
93+
i18n: {
94+
supportedLanguages: {
95+
en,
96+
es,
97+
he,
98+
},
8999
},
90100
typescript: {
91101
outputFile: path.resolve(dirname, 'payload-types.ts'),

test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,92 @@ describe('Lexical Fully Featured', () => {
208208
await expect(codeBlock.locator('.monaco-editor .view-overlays .squiggly-error')).toHaveCount(0)
209209
})
210210
})
211+
212+
describe('Lexical Fully Featured, admin panel in RTL', () => {
213+
let lexical: LexicalHelpers
214+
beforeAll(async ({ browser }, testInfo) => {
215+
testInfo.setTimeout(TEST_TIMEOUT_LONG)
216+
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
217+
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
218+
219+
const page = await browser.newPage()
220+
await ensureCompilationIsDone({ page, serverURL })
221+
await page.close()
222+
})
223+
beforeEach(async ({ page }) => {
224+
const url = new AdminUrlUtil(serverURL, lexicalFullyFeaturedSlug)
225+
lexical = new LexicalHelpers(page)
226+
await page.goto(url.account)
227+
await page.locator('.payload-settings__language .react-select').click()
228+
const options = page.locator('.rs__option')
229+
await options.locator('text=עברית').click()
230+
await expect(page.getByText('משתמשים').first()).toBeVisible()
231+
await page.goto(url.create)
232+
await lexical.editor.first().focus()
233+
})
234+
test('slash menu should be positioned correctly in RTL', async ({ page }) => {
235+
await page.keyboard.type('/')
236+
const menu = page.locator('#slash-menu .slash-menu-popup')
237+
await expect(menu).toBeVisible()
238+
239+
// left edge (0 indents)
240+
const menuBox = (await menu.boundingBox())!
241+
const slashBox = (await lexical.paragraph.getByText('/', { exact: true }).boundingBox())!
242+
await expect(() => {
243+
expect(menuBox.x).toBeGreaterThan(0)
244+
expect(menuBox.x).toBeLessThan(slashBox.x)
245+
}).toPass({ timeout: 100 })
246+
await page.keyboard.press('Backspace')
247+
await expect(menu).toBeHidden()
248+
249+
// A bit separated (3 tabs)
250+
for (let i = 0; i < 3; i++) {
251+
await page.keyboard.press('Tab')
252+
}
253+
await page.keyboard.type('/')
254+
await expect(menu).toBeVisible()
255+
const menuBox2 = (await menu.boundingBox())!
256+
const slashBox2 = (await lexical.paragraph.getByText('/', { exact: true }).boundingBox())!
257+
await expect(() => {
258+
expect(menuBox2.x).toBe(menuBox.x)
259+
// indents should allways be 40px. Please don't change this! https://github.com/payloadcms/payload/pull/13274
260+
expect(slashBox2.x).toBe(slashBox.x + 40 * 3)
261+
}).toPass({ timeout: 100 })
262+
await page.keyboard.press('Backspace')
263+
await expect(menu).toBeHidden()
264+
265+
// middle approx (13 tabs)
266+
for (let i = 0; i < 10; i++) {
267+
await page.keyboard.press('Tab')
268+
}
269+
await page.keyboard.type('/')
270+
await expect(menu).toBeVisible()
271+
const menuBox3 = (await menu.boundingBox())!
272+
const slashBox3 = (await lexical.paragraph.getByText('/', { exact: true }).boundingBox())!
273+
await expect(() => {
274+
// The right edge of the menu should be approximately the same as the left edge of the slash
275+
expect(menuBox3.x + menuBox3.width).toBeLessThan(slashBox3.x + 15)
276+
expect(menuBox3.x + menuBox3.width).toBeGreaterThan(slashBox3.x - 15)
277+
// indents should allways be 40px. Please don't change this! https://github.com/payloadcms/payload/pull/13274
278+
expect(slashBox3.x).toBe(slashBox.x + 40 * 13)
279+
}).toPass({ timeout: 100 })
280+
await page.keyboard.press('Backspace')
281+
await expect(menu).toBeHidden()
282+
283+
// right edge (27 tabs)
284+
for (let i = 0; i < 14; i++) {
285+
await page.keyboard.press('Tab')
286+
}
287+
await page.keyboard.type('/')
288+
await expect(menu).toBeVisible()
289+
const menuBox4 = (await menu.boundingBox())!
290+
const slashBox4 = (await lexical.paragraph.getByText('/', { exact: true }).boundingBox())!
291+
await expect(() => {
292+
// The right edge of the menu should be approximately the same as the left edge of the slash
293+
expect(menuBox4.x + menuBox4.width).toBeLessThan(slashBox4.x + 15)
294+
expect(menuBox4.x + menuBox4.width).toBeGreaterThan(slashBox4.x - 15)
295+
// indents should allways be 40px. Please don't change this! https://github.com/payloadcms/payload/pull/13274
296+
expect(slashBox4.x).toBe(slashBox.x + 40 * 27)
297+
}).toPass({ timeout: 100 })
298+
})
299+
})

test/lexical/payload-types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export interface Config {
142142
globalsSelect: {
143143
tabsWithRichText: TabsWithRichTextSelect<false> | TabsWithRichTextSelect<true>;
144144
};
145-
locale: 'en' | 'es';
145+
locale: 'en' | 'es' | 'he';
146146
user: User & {
147147
collection: 'users';
148148
};
@@ -1666,6 +1666,6 @@ export interface Auth {
16661666

16671667

16681668
declare module 'payload' {
1669-
// @ts-ignore
1669+
// @ts-ignore
16701670
export interface GeneratedTypes extends Config {}
1671-
}
1671+
}

test/localization-rtl/e2e.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// I'm not sure why this suite exists without tests.
2+
// I left some RTL tests in the Lexical suite because they were very lexical-related.
3+
// See: test/lexical/collections/_LexicalFullyFeatured/e2e.spec.ts
4+
// I thought about deleting this folder, but maybe it will be useful to someone in the future.

0 commit comments

Comments
 (0)