Skip to content

fix: render link/action values to the shape exporters expect#15

Merged
ivoIturrieta merged 1 commit into
mainfrom
fix/link-href-shape
May 8, 2026
Merged

fix: render link/action values to the shape exporters expect#15
ivoIturrieta merged 1 commit into
mainfrom
fix/link-href-shape

Conversation

@ivoIturrieta
Copy link
Copy Markdown
Collaborator

@ivoIturrieta ivoIturrieta commented May 8, 2026

Button.href, Image.action, Menu items, Video.href and Timer.action all rendered with href="" (or no anchor at all) regardless of the shape the caller passed. The schema stores links as { name, values: { href, target } } but exporters read e.url, so the value never reached the rendered HTML. The editor avoids this by running the link property-editor's renderValue() between storage and exporter; React-elements skipped that step.

Add normalizeLinkValue (string / { url } / { name, values } -> render shape) and normalizeValuesForExporter which walks known link-bearing paths (top-level href/action, menu.items[].link). Call it from createItemComponent between mergeValues and the exporter handoff. The renderToJson path uses the propMapper directly and is untouched, so JSON output still round-trips into the editor with the storage shape intact.

Also extend the existing string shorthand in mapSemanticProps to cover action (was only href), so a string <Image action="..." /> is wrapped to the same storage shape before merge.

Tests:

  • 11 new unit tests for the helpers in semantic-props.test.ts
  • 4 regression tests in Button.test.tsx (string / storage / values escape hatch / email mode)
  • 3 regression tests in Image.test.tsx (string / storage / email mode)
  • snapshots regenerated; only diff is target="_blank" now correctly emitted on default-href anchors, matching what the editor produces

Verified end-to-end: 275 unit tests pass, packed tarball smoke tests pass, live Next.js RSC dev server returns correct hrefs.

Summary

What does this PR do? Keep it brief.

Changes

Test Plan

  • pnpm build passes
  • pnpm test passes
  • Tested in Storybook (if UI change)
  • Added/updated tests for new behavior

Notes

Any additional context for reviewers.


📖 Storybook Preview: https://unlayer.github.io/elements/pr/15/

Button.href, Image.action, Menu items, Video.href and Timer.action all
rendered with href="" (or no anchor at all) regardless of the shape the
caller passed. The schema stores links as `{ name, values: { href, target } }`
but exporters read `e.url`, so the value never reached the rendered HTML.
The editor avoids this by running the link property-editor's `renderValue()`
between storage and exporter; React-elements skipped that step.

Add `normalizeLinkValue` (string / `{ url }` / `{ name, values }` -> render
shape) and `normalizeValuesForExporter` which walks known link-bearing
paths (top-level `href`/`action`, `menu.items[].link`). Call it from
`createItemComponent` between `mergeValues` and the exporter handoff. The
`renderToJson` path uses the propMapper directly and is untouched, so JSON
output still round-trips into the editor with the storage shape intact.

Also extend the existing string shorthand in `mapSemanticProps` to cover
`action` (was only `href`), so a string `<Image action="..." />` is wrapped
to the same storage shape before merge.

Tests:
- 11 new unit tests for the helpers in semantic-props.test.ts
- 4 regression tests in Button.test.tsx (string / storage / values escape
  hatch / email mode)
- 3 regression tests in Image.test.tsx (string / storage / email mode)
- snapshots regenerated; only diff is `target="_blank"` now correctly
  emitted on default-href anchors, matching what the editor produces

Verified end-to-end: 275 unit tests pass, packed tarball smoke tests
pass, live Next.js RSC dev server returns correct hrefs.
Copilot AI review requested due to automatic review settings May 8, 2026 18:09
@ivoIturrieta ivoIturrieta merged commit 9767efd into main May 8, 2026
4 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes React renderer output so link-bearing fields (e.g., Button.href, Image.action, Menu item links) are handed to exporters in the render-value shape they actually read ({ url, target, ... }), instead of the editor/schema storage shape ({ name, values: { href, target } }), preventing href="" / missing anchors in rendered HTML.

Changes:

  • Extend mapSemanticProps string shorthand wrapping to cover both href and action (wrapping into the schema storage shape).
  • Add normalizeLinkValue + normalizeValuesForExporter helpers in shared utils and call normalization in createItemComponent before invoking exporters.
  • Add/adjust regression tests and regenerate snapshots to reflect correct anchor attributes and Menu link rendering.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/shared/src/utils/semantic-props.ts Adds link normalization helpers and extends mapper shorthand wrapping.
packages/shared/src/utils/semantic-props.test.ts Adds unit tests for new normalization helpers and updated mapper expectations.
packages/shared/src/index.ts Re-exports the new normalization helpers from the shared package.
packages/react/src/utils/create-component.tsx Normalizes values before passing them to exporters during React render.
packages/react/src/components/Image.test.tsx Adds regression tests to ensure action renders to an <a href="...">.
packages/react/src/components/Button.test.tsx Adds regression tests to ensure href reaches rendered <a href="..."> across shapes/modes.
packages/react/src/components/snapshots/snapshots.test.tsx.snap Snapshot updates for correct link target emission.
packages/react/src/snapshots/golden-template.test.tsx.snap Snapshot updates showing Menu items now render as anchors with correct href/target.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +240 to +246
// Storage shape from the schema: { name, values: { href, target } }.
if ("name" in v && v.values && typeof v.values === "object") {
const inner = v.values as Record<string, any>;
return {
url: inner.href ?? "",
target: inner.target ?? "_blank",
...(v.attrs ?? {}),
Comment on lines +273 to +286
it("resolves storage shape ({name, values: {href, target}}) to render shape", () => {
expect(
normalizeLinkValue({
name: "web",
values: { href: "https://example.com", target: "_blank" },
})
).toEqual({ url: "https://example.com", target: "_blank" });
});

it("defaults missing target to '_blank'", () => {
expect(
normalizeLinkValue({ name: "email", values: { href: "mailto:a@b.com" } })
).toEqual({ url: "mailto:a@b.com", target: "_blank" });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants