fix(admin): sync inline-editable DOM text imperatively
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m45s
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m45s
React 19 does not reconcile children of contentEditable elements after mount, so fields whose useField value arrives async (on most paths under the tabs wrapper) stayed visually empty even though the form state had the right value. Drive textContent from a useLayoutEffect keyed on value, skip updates while the element has focus (user typing), and let commit() write back on blur. Same fix applied to PriceEditable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f5671008a7
commit
8f3a26e883
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useField } from '@payloadcms/ui'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
|
||||
type Props = {
|
||||
path: string
|
||||
@ -15,15 +15,22 @@ export function InlineEditable({ path, as: Tag = 'span', className, multiline, p
|
||||
const { value, setValue } = useField<string>({ path })
|
||||
const ref = useRef<HTMLElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.textContent !== (value ?? '')) {
|
||||
ref.current.textContent = value ?? ''
|
||||
}
|
||||
// Sync textContent from value. React doesn't reconcile children of
|
||||
// contentEditable elements, so we drive the DOM imperatively — and
|
||||
// never clobber the DOM while the user is actively typing in it.
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
if (typeof document !== 'undefined' && document.activeElement === el) return
|
||||
const next = value ?? ''
|
||||
if (el.textContent !== next) el.textContent = next
|
||||
el.classList.toggle('rebours-editable--empty', !value)
|
||||
}, [value])
|
||||
|
||||
const commit = (e: React.FocusEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => {
|
||||
const commit = (e: React.FocusEvent<HTMLElement>) => {
|
||||
const next = (e.currentTarget.textContent ?? '').replace(/\u00A0/g, ' ')
|
||||
if (next !== (value ?? '')) setValue(next)
|
||||
e.currentTarget.classList.toggle('rebours-editable--empty', !next)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
|
||||
@ -42,7 +49,7 @@ export function InlineEditable({ path, as: Tag = 'span', className, multiline, p
|
||||
return (
|
||||
<Tag
|
||||
ref={ref as never}
|
||||
className={`rebours-editable${className ? ' ' + className : ''}${!value ? ' rebours-editable--empty' : ''}`}
|
||||
className={`rebours-editable${className ? ' ' + className : ''}`}
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
@ -51,8 +58,6 @@ export function InlineEditable({ path, as: Tag = 'span', className, multiline, p
|
||||
onPaste={onPaste}
|
||||
data-placeholder={placeholder ?? ''}
|
||||
data-as={Tag}
|
||||
>
|
||||
{value ?? ''}
|
||||
</Tag>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useField } from '@payloadcms/ui'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
|
||||
function formatEuros(cents: number | null | undefined): string {
|
||||
if (cents == null) return ''
|
||||
@ -22,10 +22,13 @@ export function PriceEditable() {
|
||||
const { value: currency } = useField<string>({ path: 'currency' })
|
||||
const ref = useRef<HTMLSpanElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
if (typeof document !== 'undefined' && document.activeElement === el) return
|
||||
const rendered = formatEuros(value)
|
||||
if (ref.current.textContent !== rendered) ref.current.textContent = rendered
|
||||
if (el.textContent !== rendered) el.textContent = rendered
|
||||
el.classList.toggle('rebours-editable--empty', value == null)
|
||||
}, [value])
|
||||
|
||||
const commit = (e: React.FocusEvent<HTMLSpanElement>) => {
|
||||
@ -33,6 +36,7 @@ export function PriceEditable() {
|
||||
const cents = parseEuros(txt)
|
||||
if (cents !== value) setValue(cents)
|
||||
e.currentTarget.textContent = formatEuros(cents)
|
||||
e.currentTarget.classList.toggle('rebours-editable--empty', cents == null)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLSpanElement>) => {
|
||||
@ -48,16 +52,14 @@ export function PriceEditable() {
|
||||
<span className="checkout-price">
|
||||
<span
|
||||
ref={ref}
|
||||
className={`rebours-editable${!value ? ' rebours-editable--empty' : ''}`}
|
||||
className="rebours-editable"
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
spellCheck={false}
|
||||
onBlur={commit}
|
||||
onKeyDown={onKeyDown}
|
||||
data-placeholder="—"
|
||||
>
|
||||
{formatEuros(value)}
|
||||
</span>{' '}
|
||||
/>{' '}
|
||||
{symbol}
|
||||
</span>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user