deterministic + unique ids in 6 components (node-derived scope)
Migrates Menu, ColumnLayout, Countdown, Gallery, Tabs, InputField, and
TextareaField from Math.random()/content-hash scope ids to the threaded
Craft node id (via scopeId()), keeping every emitted element id,
aria-controls/aria-labelledby/for, and inline <script> function
name/getElementById() call consistently scoped per component.
Resolves the two Important id-collision review findings:
- Tabs: tabId was djb2(anchorId||labels) -- two default Tabs instances
produced identical aria-controls/aria-labelledby ids, so one instance's
arrow-key script clobbered the other's tab/panel wiring.
- InputField/TextareaField: fieldId was `field-${name}` -- two fields
sharing a (often default) name produced duplicate <label for>/<input
id> pairs, breaking the for/id association for one of them.
Also eliminates the remaining Math.random() scope ids in Menu (hover CSS
class scope) and ColumnLayout (nth-child width CSS class scope), so no
export component is non-deterministic anymore.
All fall back to a stable content hash (never Math.random) for legacy
2-arg toHtml() call sites without a node id.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { CSSProperties } from 'react';
|
||||
import { useNode, Element, UserComponent } from '@craftjs/core';
|
||||
import { Container } from './Container';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeAttr } from '../../utils/escape';
|
||||
import { escapeAttr, scopeId } from '../../utils/escape';
|
||||
|
||||
type SplitOption =
|
||||
| '100'
|
||||
@@ -114,7 +114,7 @@ ColumnLayout.craft = {
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
||||
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string, nodeId?: string) => {
|
||||
const columns = props.columns || 2;
|
||||
const split = props.split || '50-50';
|
||||
const gap = props.gap || '16px';
|
||||
@@ -135,13 +135,15 @@ ColumnLayout.craft = {
|
||||
// !important, to win over any stale inline flex baked into a child at
|
||||
// creation time) to a generated class -- same width mapping (getWidths)
|
||||
// the editor render uses. Precedent: Menu/Navbar toHtml already emit
|
||||
// scoped <style> blocks for hover CSS.
|
||||
const scopeId = `cols-${Math.random().toString(36).slice(2, 8)}`;
|
||||
// scoped <style> blocks for hover CSS. The class is scoped on the Craft
|
||||
// node id so two ColumnLayout instances with identical columns/split/gap
|
||||
// don't collide on the same class and cross-apply each other's widths.
|
||||
const scope = scopeId(nodeId, `${columns}:${split}:${gap}`, 'cols');
|
||||
const widthCss = widths
|
||||
.map((w, i) => `.${scopeId} > :nth-child(${i + 1}) { flex: 0 0 calc(${w} - ${gap}) !important; }`)
|
||||
.map((w, i) => `.${scope} > :nth-child(${i + 1}) { flex: 0 0 calc(${w} - ${gap}) !important; }`)
|
||||
.join('\n ');
|
||||
|
||||
return {
|
||||
html: `<style>\n ${widthCss}\n</style>\n<div class="${scopeId}"${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
||||
html: `<style>\n ${widthCss}\n</style>\n<div class="${scope}"${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user