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:
@@ -1,7 +1,7 @@
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl, scopeId } from '../../utils/escape';
|
||||
|
||||
/* ---------- Types ---------- */
|
||||
|
||||
@@ -124,7 +124,7 @@ Menu.craft = {
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(Menu as any).toHtml = (props: MenuProps, _childrenHtml: string) => {
|
||||
(Menu as any).toHtml = (props: MenuProps, _childrenHtml: string, nodeId?: string) => {
|
||||
const linkCol = props.linkColor || '#3f3f46';
|
||||
const hoverCol = props.linkHoverColor || '#3b82f6';
|
||||
const ctaBg = props.ctaBgColor || '#3b82f6';
|
||||
@@ -150,12 +150,15 @@ Menu.craft = {
|
||||
|
||||
const links = props.links || defaultLinks;
|
||||
|
||||
// Unique ID suffix for scoped CSS
|
||||
const scopeId = `menu-${Math.random().toString(36).slice(2, 8)}`;
|
||||
// Scope for the hover CSS classes below. Deterministic AND unique: scoped
|
||||
// on the Craft node id so two Menu instances with identical/default links
|
||||
// don't collide on the same `.menu-link`/`.menu-cta` class names (which
|
||||
// would let one instance's hover styling bleed into the other's).
|
||||
const scope = scopeId(nodeId, JSON.stringify(links) + orientation + alignment, 'menu');
|
||||
|
||||
const linksHtml = links.map((link) => {
|
||||
const target = link.isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||
const cls = link.isCta ? `${scopeId}-cta` : `${scopeId}-link`;
|
||||
const cls = link.isCta ? `${scope}-cta` : `${scope}-link`;
|
||||
const linkStyle = cssPropsToString({
|
||||
textDecoration: 'none',
|
||||
fontSize: fSize,
|
||||
@@ -170,8 +173,8 @@ Menu.craft = {
|
||||
}).join('\n ');
|
||||
|
||||
const hoverCss = `<style>
|
||||
.${scopeId}-link:hover { color: ${hoverCol} !important; }
|
||||
.${scopeId}-cta:hover { filter: brightness(1.1); }
|
||||
.${scope}-link:hover { color: ${hoverCol} !important; }
|
||||
.${scope}-cta:hover { filter: brightness(1.1); }
|
||||
</style>`;
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user